{"id":51015111,"url":"https://github.com/securityronin/trash-forensic","last_synced_at":"2026-06-21T09:02:51.025Z","repository":{"id":365874091,"uuid":"1274015499","full_name":"SecurityRonin/trash-forensic","owner":"SecurityRonin","description":"Read-only Windows Recycle Bin $I index reader + forensic anomaly analyzer (purged content, path-traversal names, missing deletion times) emitting forensicnomicon findings","archived":false,"fork":false,"pushed_at":"2026-06-19T08:18:06.000Z","size":71,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T10:16:28.254Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://securityronin.github.io/trash-forensic/","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-19T05:23:11.000Z","updated_at":"2026-06-19T08:18:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SecurityRonin/trash-forensic","commit_stats":null,"previous_names":["securityronin/trash-forensic"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/SecurityRonin/trash-forensic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Ftrash-forensic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Ftrash-forensic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Ftrash-forensic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Ftrash-forensic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SecurityRonin","download_url":"https://codeload.github.com/SecurityRonin/trash-forensic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Ftrash-forensic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34603657,"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":[],"created_at":"2026-06-21T09:02:50.185Z","updated_at":"2026-06-21T09:02:51.017Z","avatar_url":"https://github.com/SecurityRonin.png","language":"Rust","funding_links":["https://github.com/sponsors/h4x0r"],"categories":[],"sub_categories":[],"readme":"# trash-forensic\n\n[![trash-core](https://img.shields.io/crates/v/trash-core.svg?label=trash-core)](https://crates.io/crates/trash-core)\n[![trash-forensic](https://img.shields.io/crates/v/trash-forensic.svg?label=trash-forensic)](https://crates.io/crates/trash-forensic)\n[![Docs.rs](https://img.shields.io/docsrs/trash-forensic)](https://docs.rs/trash-forensic)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)\n[![Sponsor](https://img.shields.io/badge/sponsor-h4x0r-ea4aaa?logo=github-sponsors)](https://github.com/sponsors/h4x0r)\n\n[![CI](https://github.com/SecurityRonin/trash-forensic/actions/workflows/ci.yml/badge.svg)](https://github.com/SecurityRonin/trash-forensic/actions/workflows/ci.yml)\n[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance)\n[![security advisories](https://img.shields.io/badge/security-cargo--deny-success.svg)](deny.toml)\n\n**Who deleted what, when — recovered from the trash of every major OS, with the suspicious entries already graded for you.** Point it at a Windows `$Recycle.Bin`, a Linux XDG trash, a macOS Trash `.DS_Store`, an Android `.trashed-` file, or an iOS `Photos.sqlite` carved from an image, and get back, per deleted item: where it came from, when it was deleted, and a severity-graded finding for anything that looks tampered with.\n\n## The results, in 12 lines\n\n```toml\n[dependencies]\ntrash-forensic = \"0.2\"   # pulls in trash-core; all five OS readers on by default\n```\n\n```rust\nuse trash_core::{parse_index, scan_pairs};\nuse trash_forensic::audit_pair;\n\nfor pair in scan_pairs(recycle_bin_dir)? {           // $Recycle.Bin\\\u003cSID\u003e\\\n    let bytes = std::fs::read(\u0026pair.index_path)?;\n    if let Ok(index) = parse_index(\u0026bytes) {\n        // what was deleted, and when\n        println!(\"{} ({} bytes) deleted {:?}\",\n            index.original_path, index.original_size, index.deleted_at);\n        // …and anything suspicious about it, already graded\n        for finding in audit_pair(\u0026index, \u0026pair) {\n            println!(\"  [{:?}] {} — {}\", finding.severity, finding.code, finding.note);\n        }\n    }\n}\n# Ok::\u003c(), std::io::Error\u003e(())\n```\n\n```text\nC:\\Users\\victim\\Documents\\secret plan.docx (1234 bytes) deleted Some(2024-01-15T10:30:00Z)\n  [High] RECYCLEBIN-PATH-TRAVERSAL — stored original path ..\\..\\Windows\\…  contains parent-directory ('..') components — consistent with a crafted name rather than a normal deletion\n```\n\nEvery reader follows the same shape — decode the artifact to a deleted-item record, then grade it. A clean record prints its line and no finding.\n\n## Five operating systems, one vocabulary\n\n\"Trash\" is the genus; each platform keeps its native artifact and entry point. Each reader module is gated behind a same-named Cargo feature (all on by default), so a single-platform consumer can `--no-default-features --features \u003cos\u003e` and drop the rest's dependencies.\n\n| OS | Artifact | Reader |\n|---|---|---|\n| **Windows** | `$Recycle.Bin\\\u003cSID\u003e\\` `$I` index ⇄ `$R` content | [`windows::parse_index`] + [`scan_pairs`] |\n| **Linux** | freedesktop.org / XDG `Trash/info/*.trashinfo` ⇄ `files/` | [`linux::parse_trashinfo`] + [`scan_trash`] |\n| **macOS** | Trash `.DS_Store` put-back records (`ptbN`/`ptbL`) | [`macos::parse_put_back`] |\n| **Android** | `MediaStore` `.trashed-\u003cexpiry\u003e-\u003cname\u003e` filename codec | [`android::parse_trashed_name`] |\n| **iOS** | `Photos.sqlite` Recently Deleted (`ZASSET.ZTRASHEDSTATE`) | [`ios::parse_trashed_assets`] |\n\n[`windows::parse_index`]: https://docs.rs/trash-core/latest/trash_core/windows/fn.parse_index.html\n[`scan_pairs`]: https://docs.rs/trash-core/latest/trash_core/windows/fn.scan_pairs.html\n[`linux::parse_trashinfo`]: https://docs.rs/trash-core/latest/trash_core/linux/fn.parse_trashinfo.html\n[`scan_trash`]: https://docs.rs/trash-core/latest/trash_core/linux/fn.scan_trash.html\n[`macos::parse_put_back`]: https://docs.rs/trash-core/latest/trash_core/macos/fn.parse_put_back.html\n[`android::parse_trashed_name`]: https://docs.rs/trash-core/latest/trash_core/android/fn.parse_trashed_name.html\n[`ios::parse_trashed_assets`]: https://docs.rs/trash-core/latest/trash_core/ios/fn.parse_trashed_assets.html\n\n## What gets flagged\n\nEach finding is an **observation** (\"consistent with …\"); the examiner draws the conclusions. The codes are a stable, published contract.\n\n| Code | Category | Severity | Platforms | What it observes |\n|---|---|---|---|---|\n| `RECYCLEBIN-CONTENT-PURGED` | Residue | Medium | Windows | `$I` metadata survives but the `$R` content file is gone |\n| `RECYCLEBIN-PATH-TRAVERSAL` | Concealment | High | Windows | stored path escapes its directory via a `..` component |\n| `RECYCLEBIN-DELETION-TIME-MISSING` | Integrity | Low | Windows | the deletion `FILETIME` is zero — never set or cleared |\n| `TRASH-CONTENT-PURGED` | Residue | Medium | Linux | a `.trashinfo` survives but its `files/` content is gone |\n| `TRASH-PATH-TRAVERSAL` | Concealment | High | Linux | the stored `Path=` contains a spec-forbidden `..` |\n| `TRASH-DELETION-TIME-MISSING` | Integrity | Medium | Linux, iOS | the deletion timestamp is absent or unparseable |\n| `TRASH-ORPHAN-METADATA` | Residue | Medium | macOS | a `.DS_Store` put-back record survives but its item is gone |\n| `TRASH-PUTBACK-TRAVERSAL` | Concealment | High | macOS | the stored `ptbN`/`ptbL` escapes its directory via `..` |\n| `TRASH-EXPIRED-RESIDUE` | Residue | Low | Android, iOS | the item is still present past its retention/expiry window |\n| `TRASH-MALFORMED-NAME` | Structure | Low | Android | a `trashed`/`pending` name that does not parse to a token |\n\nFindings carry the offending value as evidence and are stamped with the analyzer name, version, and per-item scope, so they aggregate uniformly with every other [`forensicnomicon`](https://crates.io/crates/forensicnomicon) analyzer in the fleet.\n\n## No-Rust path\n\nThe two crates are the building blocks; for an end-to-end timeline that correlates trash evidence with the rest of an image, they feed [`issen`](https://github.com/SecurityRonin/issen) — the SecurityRonin examiner front end — so you get the findings without writing any Rust.\n\n## The two-crate split\n\n- **[`trash-core`](https://crates.io/crates/trash-core)** — the readers. One module per OS (`windows`, `linux`, `macos`, `android`, `ios`), each decoding its native artifact to a typed record and pairing metadata with content. No findings. The iOS reader builds on the pure-Rust [`sqlite-core`](https://crates.io/crates/sqlite-core) engine (no `libsqlite3`).\n- **[`trash-forensic`](https://crates.io/crates/trash-forensic)** — the analyzers. Grade a parsed record + its pairing into canonical `forensicnomicon` findings. The split mirrors `ntfs-core`/`ntfs-forensic`.\n\n## Trust, but verify\n\nEvery reader treats its input as attacker-controlled. The binary parsers (`$I`, `.DS_Store`) use bounds-checked reads, cap allocations against hostile length fields, walk the macOS B-tree with a cycle guard, and return a typed error — carrying the offending value — rather than panicking. `unsafe` is **forbidden** workspace-wide. Each untrusted-input reader has a `cargo fuzz` target with a *must-not-panic* invariant.\n\nCorrectness is checked against **independent oracles**, not only self-consistent round-trips:\n\n- **Windows** — fixtures built from the libyal [*Windows Recycle.Bin file formats*](https://github.com/libyal/dtformats/blob/main/documentation/Windows%20Recycle.Bin%20file%20formats.asciidoc) spec, cross-decoded with [rifiuti2](https://github.com/abelcheung/rifiuti2).\n- **Linux** — the freedesktop.org [Trash Specification v1.0](https://specifications.freedesktop.org/trash/latest/); percent-decode and date parsing cross-checked against Python `urllib`/`datetime`.\n- **macOS** — a `.DS_Store` minted by al45tair's [`ds_store`](https://pypi.org/project/ds_store/) library; decode of a real `~/.Trash/.DS_Store` agrees with that oracle **byte-for-byte across 62 put-back records**.\n- **Android** — the codec's match/split decisions agree with AOSP `FileUtils.java` `PATTERN_EXPIRES_FILE` run as a regex oracle.\n- **iOS** — a real `Photos.sqlite` decoded by both this reader and the `sqlite3` CLI, agreeing on filename and `ZTRASHEDDATE`.\n\nSee [`docs/validation.md`](docs/validation.md).\n\n---\n\n[Privacy Policy](https://securityronin.github.io/trash-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/trash-forensic/terms/) · © 2026 Security Ronin Ltd\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Ftrash-forensic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsecurityronin%2Ftrash-forensic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Ftrash-forensic/lists"}