1#![cfg_attr(
2 test,
3 allow(
4 clippy::unwrap_used,
5 clippy::expect_used,
6 clippy::no_effect_underscore_binding
7 )
8)]
9pub mod analyze;
12pub mod sqlite;
13pub mod test_utils;
14pub mod timestamp;
15
16use std::collections::HashMap;
17use std::path::Path;
18
19use serde::{Deserialize, Serialize};
20
21pub use forensicnomicon::evidence::EvidenceStrength;
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum BrowserFamily {
26 Chromium,
27 Firefox,
28 Safari,
29}
30
31impl std::fmt::Display for BrowserFamily {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::Chromium => write!(f, "Chromium"),
35 Self::Firefox => write!(f, "Firefox"),
36 Self::Safari => write!(f, "Safari"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum ArtifactKind {
44 History,
45 Cookies,
46 Downloads,
47 Extensions,
48 LoginData,
49 Cache,
50 Bookmarks,
51 Autofill,
52 Session,
53 Integrity,
54 Carved,
55 Memory,
56}
57
58impl std::fmt::Display for ArtifactKind {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::History => write!(f, "History"),
62 Self::Cookies => write!(f, "Cookies"),
63 Self::Downloads => write!(f, "Downloads"),
64 Self::Extensions => write!(f, "Extensions"),
65 Self::LoginData => write!(f, "LoginData"),
66 Self::Cache => write!(f, "Cache"),
67 Self::Bookmarks => write!(f, "Bookmarks"),
68 Self::Autofill => write!(f, "Autofill"),
69 Self::Session => write!(f, "Session"),
70 Self::Integrity => write!(f, "Integrity"),
71 Self::Carved => write!(f, "Carved"),
72 Self::Memory => write!(f, "Memory"),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct BrowserEvent {
80 pub timestamp_ns: i64,
81 pub browser: BrowserFamily,
82 pub artifact: ArtifactKind,
83 pub source: String,
84 pub description: String,
85 pub attrs: HashMap<String, serde_json::Value>,
86}
87
88impl BrowserEvent {
89 #[must_use]
90 pub fn new(
91 timestamp_ns: i64,
92 browser: BrowserFamily,
93 artifact: ArtifactKind,
94 source: impl Into<String>,
95 description: impl Into<String>,
96 ) -> Self {
97 Self {
98 timestamp_ns,
99 browser,
100 artifact,
101 source: source.into(),
102 description: description.into(),
103 attrs: HashMap::new(),
104 }
105 }
106
107 #[must_use]
108 pub fn with_attr(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
109 self.attrs.insert(key.into(), value);
110 self
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ForensicMeta {
117 pub artifact_id: String,
118 pub evidence_strength: Option<String>,
119 pub volatility: Option<String>,
120 pub caveats: Vec<String>,
121}
122
123impl ForensicMeta {
124 #[must_use]
127 pub fn lookup(artifact_id: &str) -> Option<Self> {
128 let desc = forensicnomicon::evidence::evidence_for(artifact_id)?;
129 Some(Self {
130 artifact_id: artifact_id.to_string(),
131 evidence_strength: desc.evidence_strength.map(|s| format!("{s:?}")),
132 volatility: desc.volatility.map(|v| format!("{v:?}")),
133 caveats: desc
134 .evidence_caveats
135 .iter()
136 .map(|c| (*c).to_string())
137 .collect(),
138 })
139 }
140}
141
142#[must_use]
146pub fn detect_browser(path: &Path) -> Option<BrowserFamily> {
147 let name = path.file_name()?.to_string_lossy().to_lowercase();
148 let path_str = path.to_string_lossy().to_lowercase();
149
150 if path_str.contains("safari") {
152 let safari_files = [
153 "history.db",
154 "cookies.db",
155 "downloads.plist",
156 "bookmarks.plist",
157 ];
158 if safari_files.contains(&name.as_str()) {
159 return Some(BrowserFamily::Safari);
160 }
161 }
162
163 let chromium_vendors = [
165 "chrome", "chromium", "edge", "brave", "opera", "vivaldi", "arc",
166 ];
167 let is_chromium_path = chromium_vendors.iter().any(|b| path_str.contains(b));
168 let chromium_files = ["history", "cookies", "login data", "web data", "bookmarks"];
169 if chromium_files.contains(&name.as_str()) && is_chromium_path {
170 return Some(BrowserFamily::Chromium);
171 }
172
173 if name == "places.sqlite" || name == "formhistory.sqlite" {
175 return Some(BrowserFamily::Firefox);
176 }
177 let firefox_files = [
178 "cookies.sqlite",
179 "logins.json",
180 "extensions.json",
181 "sessionstore.jsonlz4",
182 ];
183 if firefox_files.contains(&name.as_str())
184 && (path_str.contains("firefox") || path_str.contains("mozilla"))
185 {
186 return Some(BrowserFamily::Firefox);
187 }
188
189 None
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn detect_chrome_history() {
198 let p = Path::new("/home/user/.config/google-chrome/Default/History");
199 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
200 }
201
202 #[test]
203 fn detect_edge_history() {
204 let p = Path::new("/home/user/.config/microsoft-edge/Default/History");
205 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
206 }
207
208 #[test]
209 fn detect_firefox_places() {
210 let p = Path::new("/home/user/.mozilla/firefox/abc.default/places.sqlite");
211 assert_eq!(detect_browser(p), Some(BrowserFamily::Firefox));
212 }
213
214 #[test]
215 fn detect_firefox_cookies() {
216 let p = Path::new("/home/user/.mozilla/firefox/abc.default/cookies.sqlite");
217 assert_eq!(detect_browser(p), Some(BrowserFamily::Firefox));
218 }
219
220 #[test]
221 fn detect_unknown_returns_none() {
222 assert_eq!(detect_browser(Path::new("/tmp/foo.db")), None);
223 }
224
225 #[test]
226 fn browser_family_has_safari_variant() {
227 let _safari = BrowserFamily::Safari;
228 }
229
230 #[test]
231 fn artifact_kind_has_bookmarks() {
232 let _bk = ArtifactKind::Bookmarks;
233 }
234
235 #[test]
236 fn artifact_kind_has_autofill() {
237 let _af = ArtifactKind::Autofill;
238 }
239
240 #[test]
241 fn artifact_kind_has_session() {
242 let _s = ArtifactKind::Session;
243 }
244
245 #[test]
246 fn detect_safari_history_db() {
247 let p = Path::new("/Users/test/Library/Safari/History.db");
248 assert_eq!(detect_browser(p), Some(BrowserFamily::Safari));
249 }
250
251 #[test]
252 fn detect_brave_history() {
253 let p = Path::new(
254 "/Users/test/Library/Application Support/BraveSoftware/Brave-Browser/Default/History",
255 );
256 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
257 }
258
259 #[test]
260 fn browser_family_display() {
261 assert_eq!(format!("{}", BrowserFamily::Chromium), "Chromium");
262 assert_eq!(format!("{}", BrowserFamily::Firefox), "Firefox");
263 assert_eq!(format!("{}", BrowserFamily::Safari), "Safari");
264 }
265
266 #[test]
267 fn artifact_kind_display() {
268 assert_eq!(format!("{}", ArtifactKind::History), "History");
269 assert_eq!(format!("{}", ArtifactKind::Cookies), "Cookies");
270 assert_eq!(format!("{}", ArtifactKind::Bookmarks), "Bookmarks");
271 assert_eq!(format!("{}", ArtifactKind::Autofill), "Autofill");
272 assert_eq!(format!("{}", ArtifactKind::Session), "Session");
273 }
274
275 #[test]
276 fn browser_event_with_attr() {
277 use serde_json::json;
278 let ev = BrowserEvent::new(
279 1_000_000,
280 BrowserFamily::Chromium,
281 ArtifactKind::History,
282 "/path/to/History",
283 "example.com",
284 )
285 .with_attr("url", json!("https://example.com"));
286 assert_eq!(ev.attrs["url"], json!("https://example.com"));
287 assert_eq!(ev.timestamp_ns, 1_000_000);
288 }
289
290 #[test]
291 fn artifact_kind_has_integrity_variant() {
292 let _ik = ArtifactKind::Integrity;
293 assert_eq!(format!("{}", ArtifactKind::Integrity), "Integrity");
294 }
295
296 #[test]
297 fn artifact_kind_has_carved_variant() {
298 let _c = ArtifactKind::Carved;
299 assert_eq!(format!("{}", ArtifactKind::Carved), "Carved");
300 }
301
302 #[test]
303 fn artifact_kind_has_memory_variant() {
304 let _m = ArtifactKind::Memory;
305 assert_eq!(format!("{}", ArtifactKind::Memory), "Memory");
306 }
307
308 #[test]
309 fn forensic_meta_lookup_chrome_history() {
310 let meta = ForensicMeta::lookup("browser_chrome_history");
311 assert!(meta.is_some());
312 let meta = meta.unwrap();
313 assert_eq!(meta.artifact_id, "browser_chrome_history");
314 assert!(meta.evidence_strength.is_some());
315 }
316
317 #[test]
318 fn forensic_meta_lookup_unknown_returns_none() {
319 let meta = ForensicMeta::lookup("nonexistent_artifact_xyz");
320 assert!(meta.is_none());
321 }
322
323 #[test]
324 fn forensic_meta_all_browser_artifacts_have_profiles() {
325 let artifact_ids = [
326 "browser_chrome_history",
327 "browser_chrome_cookies",
328 "browser_chrome_downloads",
329 "browser_chrome_bookmarks",
330 "browser_chrome_extensions",
331 "browser_chrome_autofill",
332 "browser_chrome_cache",
333 "browser_chrome_session",
334 "browser_firefox_history",
335 "browser_firefox_cookies",
336 "browser_firefox_downloads",
337 "browser_safari_history",
338 ];
339
340 for id in &artifact_ids {
341 let meta = ForensicMeta::lookup(id);
342 assert!(
343 meta.is_some(),
344 "ForensicMeta::lookup({id}) should return Some"
345 );
346 }
347 }
348
349 #[test]
350 fn forensic_meta_evidence_strength_is_populated() {
351 let meta = ForensicMeta::lookup("browser_chrome_downloads").expect("should exist");
352 assert!(
353 meta.evidence_strength.is_some(),
354 "evidence_strength should be Some"
355 );
356 let strength = meta
358 .evidence_strength
359 .as_deref()
360 .expect("should have value");
361 assert!(
362 strength.contains("Strong"),
363 "Downloads should be Strong evidence, got: {strength}"
364 );
365 }
366}