{"id":51595155,"url":"https://github.com/securityronin/peripheral-forensic","last_synced_at":"2026-07-11T18:01:36.722Z","repository":{"id":367934614,"uuid":"1268033316","full_name":"SecurityRonin/peripheral-forensic","owner":"SecurityRonin","description":"External-device connection forensics — parse setupapi.dev.log into a typed DeviceConnection timeline across USB, FireWire, Thunderbolt, PCIe, eSATA, SD; classify DMA-capable vs storage vs HID threat. Pure Rust.","archived":false,"fork":false,"pushed_at":"2026-07-10T17:50:31.000Z","size":694,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-10T18:13:18.486Z","etag":null,"topics":["dfir","digital-forensics","forensics","incident-response","peripheral","removable-media","rust","thunderbolt","usb"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","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-13T04:37:23.000Z","updated_at":"2026-07-10T17:50:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SecurityRonin/peripheral-forensic","commit_stats":null,"previous_names":["securityronin/peripheral-forensic"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/SecurityRonin/peripheral-forensic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fperipheral-forensic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fperipheral-forensic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fperipheral-forensic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fperipheral-forensic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SecurityRonin","download_url":"https://codeload.github.com/SecurityRonin/peripheral-forensic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fperipheral-forensic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35370428,"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-07-11T02:00:05.354Z","response_time":104,"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","peripheral","removable-media","rust","thunderbolt","usb"],"created_at":"2026-07-11T18:01:35.100Z","updated_at":"2026-07-11T18:01:36.716Z","avatar_url":"https://github.com/SecurityRonin.png","language":"Rust","funding_links":["https://github.com/sponsors/h4x0r"],"categories":[],"sub_categories":[],"readme":"# peripheral-forensic\n\n[![peripheral-core](https://img.shields.io/crates/v/peripheral-core.svg?label=peripheral-core)](https://crates.io/crates/peripheral-core)\n[![peripheral-forensic](https://img.shields.io/crates/v/peripheral-forensic.svg?label=peripheral-forensic)](https://crates.io/crates/peripheral-forensic)\n[![Docs.rs](https://img.shields.io/docsrs/peripheral-forensic)](https://docs.rs/peripheral-forensic)\n[![Rust 1.81+](https://img.shields.io/badge/rust-1.81%2B-orange.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/peripheral-forensic/actions/workflows/ci.yml/badge.svg)](https://github.com/SecurityRonin/peripheral-forensic/actions)\n[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)\n\n**Point it at a Windows `setupapi.dev.log` and get back severity-graded external-device anomalies — the DMA-capable Thunderbolt/FireWire/PCIe attack surfaces, the removable mass storage, the BadUSB-shaped HID devices, and the OS-synthesized serials that weaken attribution — as `forensicnomicon::report::Finding`s.**\n\nTwo crates, one workspace:\n\n- **[`peripheral-core`](https://crates.io/crates/peripheral-core)** — the reader: parses `setupapi.dev.log` (Vista+) and `setupapi.log` (XP) device-installation logs into a uniform [`DeviceConnection`] stream — bus-classified, VID/PID/iSerial extracted, and every timestamp tagged authoritative-vs-inferred. Pure Rust, no `unsafe`, no regex engine, no date library.\n- **[`peripheral-forensic`](https://crates.io/crates/peripheral-forensic)** — the analyzer: turns the connection stream into severity-graded [`forensicnomicon::report::Finding`](https://crates.io/crates/forensicnomicon)s, so external-device evidence aggregates uniformly with the rest of the forensic fleet.\n\n## Audit a setupapi log in 30 seconds\n\n```toml\n[dependencies]\nperipheral-forensic = \"0.1\"   # pulls in peripheral-core\n```\n\n```rust\nuse peripheral_core::setupapi::parse_setupapi;\nuse peripheral_forensic::{audit, source};\n\nlet log = std::fs::read_to_string(r\"C:\\Windows\\INF\\setupapi.dev.log\")?;\nlet devices = parse_setupapi(\u0026log, \"setupapi.dev.log\");\n\nfor anomaly in audit(\u0026devices) {\n    let finding = anomaly.to_finding(source(\"evidence-host\"));\n    println!(\"[{:?}] {} — {}\", finding.severity, finding.code, finding.note);\n    // e.g. [Some(High)] PERIPHERAL-DMA-CAPABLE-DEVICE — a Thunderbolt device … consistent with a direct-memory-access attack surface (MITRE T1200)\n}\n# Ok::\u003c(), std::io::Error\u003e(())\n```\n\n`audit(\u0026devices)` returns the typed [`DeviceAnomaly`] stream; `audit_findings(\u0026devices, scope)` does the parse-to-`Finding` conversion in one call. A malformed or garbled log degrades line-by-line, never a panic.\n\n## The `DeviceConnection` model\n\nOne record per device-install section header. The forensic cautions are baked into the **type**, not just the docs:\n\n- **`device_serial` is the USB iSerial — a distinct field from `volume_serial`.** A filesystem volume serial and a device's hardware serial are different things; keeping them separate fields means the two can never be conflated in correlation.\n- **`serial_is_os_generated: bool`** — `true` when the instance-id serial's 2nd character is `\u0026` (e.g. `7\u00261c2c4f0a\u00260`). Windows synthesized the serial because the device exposed no real iSerial, so attribution back to a specific physical device is weaker. The OS-generated value is *not* reported as a real `device_serial`.\n- **Every timestamp is a `Stamp { value, confidence }`.** `first_install` from the setupapi section header is `Authoritative`; the registry-derived `last_arrival` / `last_removal` (the undocumented `0066` / `0067` device properties) are `Inferred` — and arrive only in v0.2.\n- **Correlation join keys** (`parent_id_prefix`, `volume_guid`, `drive_letter`, `volume_serial`, `disk_signature`) and a **threat lens** (`dma_capable`, `mitre`) round out the record.\n\n## Bus classification and the threat lenses\n\nThe enumerator (the leading token of a device instance id) classifies the [`Bus`], which drives two threat lenses:\n\n| Class | Buses | Lens | MITRE |\n|---|---|---|---|\n| **DMA-capable** | FireWire, Thunderbolt, PCIe, ExpressCard | Bus-mastering direct-memory-access attack surface | T1200 |\n| **Mass storage** (NOT DMA) | USB mass storage, eSATA, SD/MMC, SCSI/SAS, NVMe | Data staging/exfiltration, autorun payload | T1052.001 / T1091 |\n| **HID / wireless** | USB-HID, Bluetooth | Keystroke-injection (BadUSB) | T1200 |\n\neSATA is a SATA/storage transport and is **explicitly not** DMA-capable. (Caveat: SD-Express tunnels PCIe and *can* be DMA-capable; v0.1 treats bare `SD` as the legacy non-DMA SD/MMC bus — distinguishing SD-Express needs the device-capability bits the v0.2 registry source carries.)\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 | Category | What it observes |\n|---|---|---|---|\n| `PERIPHERAL-DMA-CAPABLE-DEVICE` | High | Threat | A FireWire / Thunderbolt / PCIe / ExpressCard device connected — consistent with a direct-memory-access attack surface (MITRE T1200) |\n| `PERIPHERAL-MASS-STORAGE-CONNECTED` | Medium | Threat | Removable mass storage connected — consistent with data staging/exfiltration or autorun payload delivery (MITRE T1052.001 / T1091) |\n| `PERIPHERAL-HID-DEVICE` | Medium | Threat | A human-interface device connected — consistent with keystroke-injection hardware such as BadUSB (MITRE T1200) |\n| `PERIPHERAL-OS-GENERATED-SERIAL` | Low | Integrity | The device exposed no real iSerial (Windows synthesized one) — consistent with weaker device attribution |\n\n## What's parsed (setupapi format coverage)\n\n`parse_setupapi(text, file)` handles both header grammars, with the real-world `\u003e\u003e\u003e  ` / `\u003c\u003c\u003c  ` section markers stripped:\n\n- **Vista+** — description first, timestamp last: `[Device Install (Hardware initiated) - USB\\VID_0781\u0026PID_5583\\\u003cserial\u003e 2023/04/15 14:23:11.456]`\n- **XP** — timestamp first: `[2005/05/12 12:34:56 632.5] Device Install - USB\\VID_...`\n\nVID/PID, enumerator, and iSerial are extracted from the device instance id; the section-header time becomes the authoritative `first_install`. Lines that match neither grammar are skipped — never a panic.\n\n## v0.2 roadmap: registry `Enum\\` + EVTX\n\nThe richest source — the Windows registry `SYSTEM\\CurrentControlSet\\Enum\\` keys (USBSTOR/USB serials, `ParentIdPrefix`), `MountedDevices` (volume serial / drive-letter / disk-signature correlation), the undocumented `0066` / `0067` Last-Arrival / Last-Removal device-property `FILETIME`s, and the EVTX device-connection events — requires the (unpublished) `winreg-core` and `winevt-forensic` fleet crates. They are deferred to **v0.2**; v0.1 is scoped to the fully self-contained `setupapi.dev.log` source and the complete data model.\n\n## Trust, but verify\n\nBuilt for untrusted logs acquired from potentially compromised systems:\n\n- **`#![forbid(unsafe_code)]`** across both crates — no FFI, no C bindings. It reads a log authored on Windows from any OS.\n- **Panic-free on malicious input** — parsing is lenient (lossy UTF-8) and bounds-checked; the workspace denies `clippy::unwrap_used` / `expect_used` in production code. A truncated or garbled log degrades line-by-line, never a crash.\n- **Fuzzed** — two `cargo-fuzz` targets (`setupapi` parse, `forensic` full parse→audit pipeline); a `fuzz.yml` CI workflow builds and smoke-runs each.\n- **Validated against spec-exact fixtures** — the analyzer is exercised end-to-end against `setupapi.dev.log` / `setupapi.log` fixtures matching the Microsoft SetupAPI text-log grammar, with planted DMA / mass-storage / HID / OS-generated-serial traces re-surfaced (see `forensic/tests/real_data.rs`).\n\n```bash\ncargo test\ncargo +nightly fuzz run forensic   # requires nightly + cargo-fuzz\n```\n\n## Where this fits\n\n`peripheral-forensic` is one analyzer in the SecurityRonin forensic fleet. The reader/analyzer split mirrors `ntfs-core`/`ntfs-forensic`; findings are emitted in the shared `forensicnomicon::report` vocabulary so [`issen`](https://github.com/SecurityRonin/issen) can correlate external-device evidence with disk, memory, and log artifacts.\n\n---\n\n[Privacy Policy](https://securityronin.github.io/peripheral-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/peripheral-forensic/terms/) · © 2026 Security Ronin Ltd\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Fperipheral-forensic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsecurityronin%2Fperipheral-forensic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Fperipheral-forensic/lists"}