{"id":51015063,"url":"https://github.com/securityronin/lnk-forensic","last_synced_at":"2026-06-21T09:02:37.281Z","repository":{"id":364495795,"uuid":"1268109165","full_name":"SecurityRonin/lnk-forensic","owner":"SecurityRonin","description":"Windows Shell Link (.lnk) forensics — parse target path, volume serial, MAC times, tracker machine ID; detect removable-media and network targets. Pure Rust. (JumpLists in v0.2.)","archived":false,"fork":false,"pushed_at":"2026-06-13T07:35:57.000Z","size":689,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T09:22:26.521Z","etag":null,"topics":["dfir","digital-forensics","forensics","incident-response","lnk","rust","shell-link","windows"],"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":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","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-13T06:38:15.000Z","updated_at":"2026-06-13T07:35:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SecurityRonin/lnk-forensic","commit_stats":null,"previous_names":["securityronin/lnk-forensic"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/SecurityRonin/lnk-forensic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Flnk-forensic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Flnk-forensic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Flnk-forensic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Flnk-forensic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SecurityRonin","download_url":"https://codeload.github.com/SecurityRonin/lnk-forensic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Flnk-forensic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34603634,"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":["dfir","digital-forensics","forensics","incident-response","lnk","rust","shell-link","windows"],"created_at":"2026-06-21T09:02:35.901Z","updated_at":"2026-06-21T09:02:37.269Z","avatar_url":"https://github.com/SecurityRonin.png","language":"Rust","funding_links":["https://github.com/sponsors/h4x0r"],"categories":[],"sub_categories":[],"readme":"# lnk-forensic\n\n[![Crates.io lnk-core](https://img.shields.io/crates/v/lnk-core?label=lnk-core)](https://crates.io/crates/lnk-core)\n[![Crates.io lnk-forensic](https://img.shields.io/crates/v/lnk-forensic?label=lnk-forensic)](https://crates.io/crates/lnk-forensic)\n[![Docs.rs](https://img.shields.io/docsrs/lnk-core?label=docs.rs)](https://docs.rs/lnk-core)\n[![Rust 1.81+](https://img.shields.io/badge/rust-1.81%2B-blue.svg)](https://www.rust-lang.org)\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/lnk-forensic/actions/workflows/ci.yml/badge.svg)](https://github.com/SecurityRonin/lnk-forensic/actions/workflows/ci.yml)\n[![Coverage](https://img.shields.io/badge/coverage-100%25%20lib-brightgreen.svg)](https://github.com/SecurityRonin/lnk-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-advisories%20clean-brightgreen.svg)](deny.toml)\n\n**Turn a Windows `.lnk` shortcut — or a whole Jump List — into graded forensic findings — surface the file opened from a USB stick, the share it came off, and the machine it was authored on, with the volume serial that ties it back to the physical device.**\n\nA `.lnk` is a rich `[MS-SHLLINK]` artifact: it records the target path, the volume\nserial and MAC timestamps, the origin machine's NetBIOS name, and a distributed-\nlink-tracking droid GUID — often evidence of a file that no longer exists.\n`lnk-forensic` reads it from a link authored on **any** Windows host and grades\nwhat matters for triage. It also parses **Jump Lists** — the taskbar/Start MRU\nartifact — both `*.automaticDestinations-ms` (an OLE/CFB compound file with a\n`DestList` MRU stream + one embedded `.lnk` per entry) and\n`*.customDestinations-ms` (a flat run of embedded `.lnk`s), reusing the same\nshell-link audit over every embedded link.\n\n## Audit a Shell Link in 30 seconds\n\n```toml\n[dependencies]\nlnk-forensic = \"0.2\"   # pulls in lnk-core\n```\n\n```rust\nuse lnk_core::parse_shell_link;\nuse lnk_forensic::{audit_findings};\n\n// .lnk bytes off disk; a malformed header yields None, never a panic.\nif let Some(link) = parse_shell_link(lnk_bytes) {\n    for f in audit_findings(\u0026link, \"volume: E:\") {\n        println!(\"[{:?}] {} — {}\", f.severity, f.code, f.note);\n        // e.g. [Some(Medium)] LNK-REMOVABLE-MEDIA-TARGET — the link target resolves to a removable …\n    }\n}\n```\n\nWant the typed stream instead of graded findings? `audit(\u0026link)` returns\n`Vec\u003cLnkAnomaly\u003e`; each anomaly emits a `forensicnomicon::report::Finding` via\n`to_finding(source)`.\n\n## The anomaly codes\n\nEach anomaly is an **observation** (\"consistent with …\"); the examiner draws the\nconclusions. Codes are a stable, published contract.\n\n| Code | Severity | Category | What it observes |\n|---|---|---|---|\n| `LNK-REMOVABLE-MEDIA-TARGET` | Medium | Threat | The `VolumeID` describes a `DRIVE_REMOVABLE` volume — consistent with a file opened from external media (MITRE T1052.001 / T1091). The **volume serial** is surfaced as the join key to a peripheral device connection. |\n| `LNK-NETWORK-TARGET` | Low | Threat | The link carries a `CommonNetworkRelativeLink` — consistent with a file opened from a network share (MITRE T1021). |\n| `LNK-TRACKER-MACHINE` | Info | Provenance | The `TrackerDataBlock` records the origin machine's NetBIOS name — consistent with the link having been authored on that machine (attribution). |\n\n## Jump Lists — Automatic + Custom Destinations\n\n`parse_automatic_destinations(bytes, filename)` opens a `*.automaticDestinations-ms`\nas a CFB compound file, reads the `DestList` MRU stream (Windows 7 v1 and\nWindows 10/11 v2+ layouts), and decodes each embedded `.lnk` sub-stream;\n`parse_custom_destinations(bytes, filename)` splits a flat\n`*.customDestinations-ms` into its embedded `.lnk`s by the `[MS-SHLLINK]` CLSID\nand `0xBABFFBAB` footer. `audit_jumplist(\u0026jl, acquisition_host, scope)` runs the\n**existing per-link audit over every embedded link** (so the codes above fire for\nfree) plus four Jump-List-level codes:\n\n| Code | Severity | Category | What it observes |\n|---|---|---|---|\n| `JUMPLIST-PINNED-TARGET` | Low | Provenance | A `DestList` entry is **pinned** — consistent with the user having deliberately fixed this target to the application's Jump List. |\n| `JUMPLIST-CROSS-MACHINE` | Low | Provenance | A `DestList` entry's origin hostname (or droid volume GUID) has **no match to the acquisition host** — consistent with the target/artifact having originated on a different machine. |\n| `JUMPLIST-MRU-RECENCY` | Info | History | A `DestList` entry's last-access time + access count — the application's own usage history for the target. |\n| `JUMPLIST-APPID-IDENTIFIED` | Info | Provenance | The Jump List `AppID` resolves to a known application via `forensicnomicon::jumplist::appid_name`. |\n\nThe DestList offset tables, the `0xBABFFBAB` footer, the embedded-LNK CLSID\nboundary, and the `AppID` map all come from\n[`forensicnomicon::jumplist`](https://crates.io/crates/forensicnomicon).\n\n## The volume serial is a cross-artifact join key\n\nA `.lnk`'s `VolumeID.DriveSerialNumber` is the same 32-bit volume serial a USB\nmass-storage device records in the registry / setupapi log. `lnk-forensic`\nsurfaces it first-class on the removable-media anomaly so an examiner can\n**correlate** a file opened from external media (this link) with the **physical\ndevice** that carried it (a\n[`peripheral-forensic`](https://github.com/SecurityRonin/peripheral-forensic)\n`DeviceConnection`). The serial is the join key — the link surfaces the value, the\nexaminer reconciles it.\n\n## The two-crate split\n\n- **`lnk-core`** — the reader. Parses the 0x4C `ShellLinkHeader` (LinkFlags,\n  FileAttributes, the three target FILETIMEs → Unix epoch, file size, icon index,\n  show command, hotkey), the `LinkInfo` block (`VolumeID` drive type + serial +\n  label, local base path, `CommonNetworkRelativeLink`), ANSI/Unicode `StringData`,\n  the raw `LinkTargetIDList` PIDL blob (full PIDL decode is a shellbag parser's\n  job), the `ExtraData` `TrackerDataBlock`, and **Jump Lists** (Automatic + Custom\n  Destinations). Format constants come from\n  [`forensicnomicon::shlink`](https://crates.io/crates/forensicnomicon) and\n  [`forensicnomicon::jumplist`](https://crates.io/crates/forensicnomicon); the\n  parsing algorithm lives here. No findings.\n- **`lnk-forensic`** — the analyzer. Audits a `ShellLink` or a `JumpList` into\n  graded `forensicnomicon::report::Finding`s. Depends on `lnk-core`.\n\n### Third-party dependency note\n\n`lnk-core` depends on the mature MIT-licensed\n[`cfb`](https://crates.io/crates/cfb) crate to read the OLE Compound-File\ncontainer that `*.automaticDestinations-ms` Jump Lists are stored in — a\ndocumented exception to \"prefer our own\", on the same footing as `lznt1` for\nNTFS: reusing a correct, maintained, better-scoped reader beats reinventing an\nOLE/CFB parser. Our own code stays `#![forbid(unsafe_code)]`.\n\n## Trust, but verify\n\nBuilt for untrusted `.lnk` files from potentially compromised systems:\n\n- **`#![forbid(unsafe_code)]`** across both crates — no FFI, no C bindings.\n- **Panic-free on malicious input** — every integer/length/offset read is\n  bounds-checked; the workspace denies `clippy::unwrap_used` and\n  `clippy::expect_used` in production code. A truncated or garbled link yields\n  absent sub-structures or `None`, never a crash.\n- **Fuzzed** — `cargo-fuzz` targets `shelllink` (the reader), `forensic` (the\n  full parse → audit pipeline), and `jumplist` (the CFB/DestList + custom-\n  destinations parse → audit); a `fuzz.yml` CI workflow builds and smoke-runs\n  each.\n- **Validated against spec-exact artifacts** — the pipeline is exercised\n  end-to-end against hand-authored fixtures: `[MS-SHLLINK]` links (a removable-\n  media link with a volume serial + a network-share link;\n  `forensic/tests/real_data.rs`) and Jump Lists (a real CFB\n  `*.automaticDestinations-ms` with a pinned, cross-machine removable entry + a\n  flat `*.customDestinations-ms`; `forensic/tests/jumplist.rs`), reconciling the\n  surfaced serial and findings.\n\n```bash\ncargo test\ncargo +nightly fuzz run forensic   # requires nightly + cargo-fuzz\n```\n\n## Where this fits\n\n`lnk-forensic` is a parser/analyzer in the SecurityRonin forensic fleet: each\ncrate is a deep expert in one artifact family, emitting the shared\n`forensicnomicon::report` vocabulary so findings aggregate uniformly across disk,\nmemory, log, and registry artifacts.\n\n[Privacy Policy](https://securityronin.github.io/lnk-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/lnk-forensic/terms/) · © 2026 Security Ronin Ltd\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Flnk-forensic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsecurityronin%2Flnk-forensic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Flnk-forensic/lists"}