Skip to main content

forensic_carve/
lib.rs

1//! `forensic-carve` — the SecurityRonin fleet carving contract and single-pass
2//! sweep engine.
3//!
4//! This crate owns the medium-agnostic carving *contract* — the [`Carver`] trait,
5//! [`Signature`], [`CarveContext`], [`CarvedItem`], and the [`RecoveryMethod`]
6//! provenance vocabulary — plus (in later increments) the aho-corasick sweep engine
7//! that runs one detection pass over disk-unallocated or memory regions and
8//! dispatches capped windows to the matching carver.
9//!
10//! Fleet ADR 0001 (`ronin-issen/docs/decisions/`) is the governing design. A carver
11//! sees only `&[u8]` windows and plain values, so the *same* carver serves disk and
12//! memory sweeps; medium attribution (PID/VA/PFN, volume/run ids) is wrapped by the
13//! driver *after* the call and never appears on [`CarvedItem`].
14//!
15//! Increment 1: the contract types only. The sweep engine lands in a later cycle.
16
17#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
18
19mod engine;
20mod registry;
21pub use engine::{sweep, CarveOptions, Region, RegionSource, SweptItem};
22pub use registry::{registered_carvers, CarverRegistration};
23
24/// How (and how broadly) an artifact was recovered — the fleet-wide provenance
25/// vocabulary (ADR 0001 §3). Carving *is* a recovery method, so this general
26/// concept owns the plain name; the SQLite-record substrate detail lives in
27/// `browser-forensic-carve` as `SqliteRecoveryMethod`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub enum RecoveryMethod {
31    /// A deletion the filesystem itself recorded (an `$MFT` record with `IN_USE`
32    /// cleared, an ext4 orphan inode). The `--deleted` flag.
33    Tombstone,
34    /// Tier-1 recovery from within a located artifact's own slack (freelist / WAL /
35    /// `ElfChnk`). Default-on in the parser.
36    FileInternalCarve,
37    /// Tier-2 whole-image carving of unallocated space. The `--unallocated` flag.
38    UnallocatedCarve,
39    /// Recovered from a memory image (a process VA region or a physical frame).
40    MemoryCarve,
41}
42
43impl RecoveryMethod {
44    /// A stable serialization token, used when translating provenance onto the
45    /// report model's `Evidence`/tags at orchestration (ADR 0001 §2). Never change a
46    /// shipped token.
47    #[must_use]
48    pub const fn as_str(self) -> &'static str {
49        match self {
50            RecoveryMethod::Tombstone => "tombstone",
51            RecoveryMethod::FileInternalCarve => "file-internal-carve",
52            RecoveryMethod::UnallocatedCarve => "unallocated-carve",
53            RecoveryMethod::MemoryCarve => "memory-carve",
54        }
55    }
56}
57
58/// A magic signature a carver recognises: the bytes, plus the offset at which they
59/// appear *within the artifact*, so a mid-artifact magic still anchors the window
60/// start correctly.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub struct Signature {
64    magic: &'static [u8],
65    offset: usize,
66}
67
68impl Signature {
69    /// A signature whose `magic` bytes appear `offset` bytes into the artifact.
70    #[must_use]
71    pub const fn new(magic: &'static [u8], offset: usize) -> Self {
72        Self { magic, offset }
73    }
74
75    /// The magic bytes.
76    #[must_use]
77    pub const fn magic(&self) -> &'static [u8] {
78        self.magic
79    }
80
81    /// The offset of the magic within the artifact (0 for a header magic).
82    #[must_use]
83    pub const fn offset(&self) -> usize {
84        self.offset
85    }
86}
87
88/// What the engine does with carved items given their confidence. Defaults live in
89/// each medium's driver, not hard-coded in the engine (ADR 0001 §8/C1).
90#[derive(Debug, Clone, Copy, PartialEq)]
91pub enum ConfidencePolicy {
92    /// Keep every item, even those with no or low confidence.
93    KeepAll,
94    /// Drop items whose confidence is below this floor.
95    Minimum(f32),
96}
97
98/// The plain-values context handed to [`Carver::carve`]. Carries *only* values — the
99/// window's absolute base offset in the source, and the confidence policy. It never
100/// carries a `Read`/`Seek`, a VFS handle, or a memory provider: a carver that must
101/// chase virtual pointers is a memory *walker*, not a medium-agnostic carver.
102#[derive(Debug, Clone, Copy, PartialEq)]
103#[non_exhaustive]
104pub struct CarveContext {
105    base_offset: u64,
106    policy: ConfidencePolicy,
107    method: RecoveryMethod,
108}
109
110impl CarveContext {
111    /// A context for a window whose first byte sits at `base_offset` in the source.
112    /// Defaults to the disk Tier-2 (`--unallocated`) recovery method; the driver sets
113    /// the correct one via [`CarveContext::with_method`].
114    #[must_use]
115    pub const fn at(base_offset: u64) -> Self {
116        Self {
117            base_offset,
118            policy: ConfidencePolicy::KeepAll,
119            method: RecoveryMethod::UnallocatedCarve,
120        }
121    }
122
123    /// Set the confidence policy (builder style).
124    #[must_use]
125    pub const fn with_policy(mut self, policy: ConfidencePolicy) -> Self {
126        self.policy = policy;
127        self
128    }
129
130    /// Set the recovery method the driver is carving under (builder style).
131    #[must_use]
132    pub const fn with_method(mut self, method: RecoveryMethod) -> Self {
133        self.method = method;
134        self
135    }
136
137    /// The recovery method for this sweep (the driver's medium/tier). A carver echoes
138    /// this, so the *same* carver stamps `UnallocatedCarve` on a disk sweep and
139    /// `MemoryCarve` on a memory sweep.
140    #[must_use]
141    pub const fn recovery_method(&self) -> RecoveryMethod {
142        self.method
143    }
144
145    /// The absolute offset of the window's first byte in the source.
146    #[must_use]
147    pub const fn base_offset(&self) -> u64 {
148        self.base_offset
149    }
150
151    /// The confidence policy in effect.
152    #[must_use]
153    pub const fn policy(&self) -> ConfidencePolicy {
154        self.policy
155    }
156}
157
158/// A carved item's payload: either a bounded whole artifact (which re-enters the
159/// normal classify→parse pipeline) or already-decoded records (for a loose chunk
160/// with no containing file to re-parse — e.g. an orphaned `ElfChnk`).
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum CarvedPayload {
163    /// Already-decoded records; the carver did the interpretation.
164    Records,
165    /// A bounded whole-artifact byte buffer for pipeline re-entry.
166    ArtifactBytes(Vec<u8>),
167}
168
169/// One recovered item. **Medium-neutral**: it never carries PID/VA/PFN (memory) or
170/// volume/run ids (disk) — that attribution is wrapped by the driver *after* the
171/// carve call (ADR 0001 §8, `SweptItem`). Constructed via [`CarvedItem::records`] /
172/// [`CarvedItem::artifact_bytes`], never a struct literal.
173#[derive(Debug, Clone, PartialEq)]
174#[non_exhaustive]
175pub struct CarvedItem {
176    format: &'static str,
177    image_offset: u64,
178    confidence: f32,
179    method: RecoveryMethod,
180    payload: CarvedPayload,
181}
182
183impl CarvedItem {
184    /// A records-payload item (the carver decoded the records itself).
185    #[must_use]
186    pub fn records(
187        format: &'static str,
188        image_offset: u64,
189        confidence: f32,
190        method: RecoveryMethod,
191    ) -> Self {
192        Self {
193            format,
194            image_offset,
195            confidence,
196            method,
197            payload: CarvedPayload::Records,
198        }
199    }
200
201    /// An artifact-bytes item (a bounded whole artifact for pipeline re-entry).
202    #[must_use]
203    pub fn artifact_bytes(
204        format: &'static str,
205        image_offset: u64,
206        confidence: f32,
207        method: RecoveryMethod,
208        bytes: Vec<u8>,
209    ) -> Self {
210        Self {
211            format,
212            image_offset,
213            confidence,
214            method,
215            payload: CarvedPayload::ArtifactBytes(bytes),
216        }
217    }
218
219    /// The scheme-prefixed format id (e.g. `"sqlite"`, `"evtx-chunk"`).
220    #[must_use]
221    pub const fn format(&self) -> &'static str {
222        self.format
223    }
224
225    /// The absolute offset of the item in the source.
226    #[must_use]
227    pub const fn image_offset(&self) -> u64 {
228        self.image_offset
229    }
230
231    /// The carver's confidence in this item, normalized 0.0–1.0.
232    #[must_use]
233    pub const fn confidence(&self) -> f32 {
234        self.confidence
235    }
236
237    /// How the item was recovered.
238    #[must_use]
239    pub const fn recovery_method(&self) -> RecoveryMethod {
240        self.method
241    }
242
243    /// The item's payload.
244    #[must_use]
245    pub const fn payload(&self) -> &CarvedPayload {
246        &self.payload
247    }
248}
249
250/// A per-format carver. One impl per format, living in that format's PARSER crate,
251/// seeing only `&[u8]` windows — medium-agnostic by construction, so the same carver
252/// serves disk-unallocated and memory sweeps.
253pub trait Carver: Send + Sync {
254    /// The scheme-prefixed format id this carver produces (e.g. `"sqlite"`).
255    fn format(&self) -> &'static str;
256
257    /// The magic signatures that anchor a candidate window for this format.
258    fn signatures(&self) -> &[Signature];
259
260    /// An upper bound on the bytes one hit may claim, so the engine can cap the
261    /// window it materializes.
262    fn max_window(&self) -> u64;
263
264    /// Carve a (capped) window into zero or more items. Must validate structurally
265    /// before emitting and grade confidence; a hit backed only by a short magic with
266    /// no second independent check must not emit.
267    fn carve(&self, window: &[u8], ctx: &CarveContext) -> Vec<CarvedItem>;
268}