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}