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 finding;
13pub mod reconstruct;
14pub mod sqlite;
15pub mod test_utils;
16pub mod timestamp;
17
18use std::collections::HashMap;
19use std::path::Path;
20
21use serde::{Deserialize, Serialize};
22
23pub use forensicnomicon::evidence::EvidenceStrength;
24
25pub use finding::{
26 Confidence, EvidenceSource, EvidenceState, Finding, Priority, Provenance, TimestampBasis,
27 UserActionClaim,
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33pub enum BrowserFamily {
34 Chromium,
35 Firefox,
36 Safari,
37 InternetExplorer,
40 EdgeLegacy,
43}
44
45impl std::fmt::Display for BrowserFamily {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Chromium => write!(f, "Chromium"),
49 Self::Firefox => write!(f, "Firefox"),
50 Self::Safari => write!(f, "Safari"),
51 Self::InternetExplorer => write!(f, "Internet Explorer"),
52 Self::EdgeLegacy => write!(f, "Edge (Legacy)"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60pub enum ArtifactKind {
61 History,
62 Cookies,
63 Downloads,
64 Extensions,
65 LoginData,
66 Cache,
67 Bookmarks,
68 Autofill,
69 Session,
70 Integrity,
71 Carved,
72 Memory,
73 Preferences,
74 LocalStorage,
75 Permission,
77 CreditCard,
79 AuthToken,
81 RecoveredDomain,
85 Favicon,
88 TopSite,
91 Shortcut,
94 NetworkPrediction,
97 MediaPlayback,
100 TypedInput,
104 Annotation,
108 RecoveredBookmark,
112}
113
114impl std::fmt::Display for ArtifactKind {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 Self::History => write!(f, "History"),
118 Self::Cookies => write!(f, "Cookies"),
119 Self::Downloads => write!(f, "Downloads"),
120 Self::Extensions => write!(f, "Extensions"),
121 Self::LoginData => write!(f, "LoginData"),
122 Self::Cache => write!(f, "Cache"),
123 Self::Bookmarks => write!(f, "Bookmarks"),
124 Self::Autofill => write!(f, "Autofill"),
125 Self::Session => write!(f, "Session"),
126 Self::Integrity => write!(f, "Integrity"),
127 Self::Carved => write!(f, "Carved"),
128 Self::Memory => write!(f, "Memory"),
129 Self::Preferences => write!(f, "Preferences"),
130 Self::LocalStorage => write!(f, "LocalStorage"),
131 Self::Permission => write!(f, "Permission"),
132 Self::CreditCard => write!(f, "CreditCard"),
133 Self::AuthToken => write!(f, "AuthToken"),
134 Self::RecoveredDomain => write!(f, "RecoveredDomain"),
135 Self::Favicon => write!(f, "Favicon"),
136 Self::TopSite => write!(f, "TopSite"),
137 Self::Shortcut => write!(f, "Shortcut"),
138 Self::NetworkPrediction => write!(f, "NetworkPrediction"),
139 Self::MediaPlayback => write!(f, "MediaPlayback"),
140 Self::TypedInput => write!(f, "TypedInput"),
141 Self::Annotation => write!(f, "Annotation"),
142 Self::RecoveredBookmark => write!(f, "RecoveredBookmark"),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150pub struct BrowserEvent {
151 pub timestamp_ns: i64,
152 pub browser: BrowserFamily,
153 pub artifact: ArtifactKind,
154 pub source: String,
155 pub description: String,
156 pub attrs: HashMap<String, serde_json::Value>,
157}
158
159impl BrowserEvent {
160 #[must_use]
161 pub fn new(
162 timestamp_ns: i64,
163 browser: BrowserFamily,
164 artifact: ArtifactKind,
165 source: impl Into<String>,
166 description: impl Into<String>,
167 ) -> Self {
168 Self {
169 timestamp_ns,
170 browser,
171 artifact,
172 source: source.into(),
173 description: description.into(),
174 attrs: HashMap::new(),
175 }
176 }
177
178 #[must_use]
179 pub fn with_attr(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
180 self.attrs.insert(key.into(), value);
181 self
182 }
183}
184
185#[cfg(feature = "schema")]
192#[must_use]
193pub fn browser_event_schema() -> schemars::Schema {
194 schemars::schema_for!(BrowserEvent)
195}
196
197#[cfg(feature = "schema")]
203#[must_use]
204pub fn finding_schema() -> schemars::Schema {
205 schemars::schema_for!(Finding)
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ForensicMeta {
211 pub artifact_id: String,
212 pub evidence_strength: Option<String>,
213 pub volatility: Option<String>,
214 pub caveats: Vec<String>,
215}
216
217impl ForensicMeta {
218 #[must_use]
221 pub fn lookup(artifact_id: &str) -> Option<Self> {
222 let desc = forensicnomicon::evidence::evidence_for(artifact_id)?;
223 Some(Self {
224 artifact_id: artifact_id.to_string(),
225 evidence_strength: desc.evidence_strength.map(|s| format!("{s:?}")),
226 volatility: desc.volatility.map(|v| format!("{v:?}")),
227 caveats: desc
228 .evidence_caveats
229 .iter()
230 .map(|c| (*c).to_string())
231 .collect(),
232 })
233 }
234}
235
236#[must_use]
240pub fn detect_browser(path: &Path) -> Option<BrowserFamily> {
241 let name = path.file_name()?.to_string_lossy().to_lowercase();
242 let path_str = path.to_string_lossy().to_lowercase();
243
244 if path_str.contains("safari") {
246 let safari_files = [
247 "history.db",
248 "cookies.db",
249 "downloads.plist",
250 "bookmarks.plist",
251 ];
252 if safari_files.contains(&name.as_str()) {
253 return Some(BrowserFamily::Safari);
254 }
255 }
256
257 let chromium_vendors = [
259 "chrome", "chromium", "edge", "brave", "opera", "vivaldi", "arc",
260 ];
261 let is_chromium_path = chromium_vendors.iter().any(|b| path_str.contains(b));
262 let chromium_files = ["history", "cookies", "login data", "web data", "bookmarks"];
263 if chromium_files.contains(&name.as_str()) && is_chromium_path {
264 return Some(BrowserFamily::Chromium);
265 }
266
267 if name == "places.sqlite" || name == "formhistory.sqlite" {
269 return Some(BrowserFamily::Firefox);
270 }
271 let firefox_files = [
272 "cookies.sqlite",
273 "logins.json",
274 "extensions.json",
275 "sessionstore.jsonlz4",
276 ];
277 if firefox_files.contains(&name.as_str())
278 && (path_str.contains("firefox") || path_str.contains("mozilla"))
279 {
280 return Some(BrowserFamily::Firefox);
281 }
282
283 None
284}
285
286#[cfg(all(test, feature = "schema"))]
287mod schema_tests {
288 #[test]
289 fn browser_event_schema_describes_the_event_fields() {
290 let schema = super::browser_event_schema();
291 let json = serde_json::to_value(&schema).expect("schema serializes");
292 let props = json
293 .get("properties")
294 .and_then(serde_json::Value::as_object)
295 .expect("BrowserEvent schema has a properties object");
296 for field in [
297 "timestamp_ns",
298 "browser",
299 "artifact",
300 "source",
301 "description",
302 "attrs",
303 ] {
304 assert!(
305 props.contains_key(field),
306 "schema should describe the `{field}` field of BrowserEvent"
307 );
308 }
309 let defs = json
311 .get("$defs")
312 .and_then(serde_json::Value::as_object)
313 .expect("schema exposes $defs for the sub-types");
314 assert!(defs.contains_key("BrowserFamily"), "BrowserFamily in $defs");
315 assert!(defs.contains_key("ArtifactKind"), "ArtifactKind in $defs");
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn detect_chrome_history() {
325 let p = Path::new("/home/user/.config/google-chrome/Default/History");
326 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
327 }
328
329 #[test]
330 fn detect_edge_history() {
331 let p = Path::new("/home/user/.config/microsoft-edge/Default/History");
332 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
333 }
334
335 #[test]
336 fn detect_firefox_places() {
337 let p = Path::new("/home/user/.mozilla/firefox/abc.default/places.sqlite");
338 assert_eq!(detect_browser(p), Some(BrowserFamily::Firefox));
339 }
340
341 #[test]
342 fn detect_firefox_cookies() {
343 let p = Path::new("/home/user/.mozilla/firefox/abc.default/cookies.sqlite");
344 assert_eq!(detect_browser(p), Some(BrowserFamily::Firefox));
345 }
346
347 #[test]
348 fn detect_unknown_returns_none() {
349 assert_eq!(detect_browser(Path::new("/tmp/foo.db")), None);
350 }
351
352 #[test]
353 fn browser_family_has_safari_variant() {
354 let _safari = BrowserFamily::Safari;
355 }
356
357 #[test]
358 fn artifact_kind_has_bookmarks() {
359 let _bk = ArtifactKind::Bookmarks;
360 }
361
362 #[test]
363 fn artifact_kind_has_autofill() {
364 let _af = ArtifactKind::Autofill;
365 }
366
367 #[test]
368 fn artifact_kind_has_session() {
369 let _s = ArtifactKind::Session;
370 }
371
372 #[test]
373 fn detect_safari_history_db() {
374 let p = Path::new("/Users/test/Library/Safari/History.db");
375 assert_eq!(detect_browser(p), Some(BrowserFamily::Safari));
376 }
377
378 #[test]
379 fn detect_brave_history() {
380 let p = Path::new(
381 "/Users/test/Library/Application Support/BraveSoftware/Brave-Browser/Default/History",
382 );
383 assert_eq!(detect_browser(p), Some(BrowserFamily::Chromium));
384 }
385
386 #[test]
387 fn browser_family_display() {
388 assert_eq!(format!("{}", BrowserFamily::Chromium), "Chromium");
389 assert_eq!(format!("{}", BrowserFamily::Firefox), "Firefox");
390 assert_eq!(format!("{}", BrowserFamily::Safari), "Safari");
391 }
392
393 #[test]
394 fn browser_family_display_ie_and_edge_legacy() {
395 assert_eq!(
396 format!("{}", BrowserFamily::InternetExplorer),
397 "Internet Explorer"
398 );
399 assert_eq!(format!("{}", BrowserFamily::EdgeLegacy), "Edge (Legacy)");
400 }
401
402 #[test]
403 fn artifact_kind_display() {
404 assert_eq!(format!("{}", ArtifactKind::History), "History");
405 assert_eq!(format!("{}", ArtifactKind::Cookies), "Cookies");
406 assert_eq!(format!("{}", ArtifactKind::Bookmarks), "Bookmarks");
407 assert_eq!(format!("{}", ArtifactKind::Autofill), "Autofill");
408 assert_eq!(format!("{}", ArtifactKind::Session), "Session");
409 }
410
411 #[test]
412 fn browser_event_with_attr() {
413 use serde_json::json;
414 let ev = BrowserEvent::new(
415 1_000_000,
416 BrowserFamily::Chromium,
417 ArtifactKind::History,
418 "/path/to/History",
419 "example.com",
420 )
421 .with_attr("url", json!("https://example.com"));
422 assert_eq!(ev.attrs["url"], json!("https://example.com"));
423 assert_eq!(ev.timestamp_ns, 1_000_000);
424 }
425
426 #[test]
427 fn artifact_kind_has_integrity_variant() {
428 let _ik = ArtifactKind::Integrity;
429 assert_eq!(format!("{}", ArtifactKind::Integrity), "Integrity");
430 }
431
432 #[test]
433 fn artifact_kind_has_carved_variant() {
434 let _c = ArtifactKind::Carved;
435 assert_eq!(format!("{}", ArtifactKind::Carved), "Carved");
436 }
437
438 #[test]
439 fn artifact_kind_has_memory_variant() {
440 let _m = ArtifactKind::Memory;
441 assert_eq!(format!("{}", ArtifactKind::Memory), "Memory");
442 }
443
444 #[test]
445 fn artifact_kind_has_preferences_variant() {
446 let _p = ArtifactKind::Preferences;
447 assert_eq!(format!("{}", ArtifactKind::Preferences), "Preferences");
448 }
449
450 #[test]
451 fn artifact_kind_has_local_storage_variant() {
452 let _ls = ArtifactKind::LocalStorage;
453 assert_eq!(format!("{}", ArtifactKind::LocalStorage), "LocalStorage");
454 }
455
456 #[test]
457 fn artifact_kind_has_permission_variant() {
458 let _p = ArtifactKind::Permission;
459 assert_eq!(format!("{}", ArtifactKind::Permission), "Permission");
460 }
461
462 #[test]
463 fn artifact_kind_has_credit_card_variant() {
464 let _c = ArtifactKind::CreditCard;
465 assert_eq!(format!("{}", ArtifactKind::CreditCard), "CreditCard");
466 }
467
468 #[test]
469 fn artifact_kind_has_auth_token_variant() {
470 let _t = ArtifactKind::AuthToken;
471 assert_eq!(format!("{}", ArtifactKind::AuthToken), "AuthToken");
472 }
473
474 #[test]
475 fn artifact_kind_has_recovered_domain_variant() {
476 let _r = ArtifactKind::RecoveredDomain;
477 assert_eq!(
478 format!("{}", ArtifactKind::RecoveredDomain),
479 "RecoveredDomain"
480 );
481 }
482
483 #[test]
484 fn artifact_kind_has_favicon_variant() {
485 let _f = ArtifactKind::Favicon;
486 assert_eq!(format!("{}", ArtifactKind::Favicon), "Favicon");
487 }
488
489 #[test]
490 fn artifact_kind_has_top_site_variant() {
491 let _t = ArtifactKind::TopSite;
492 assert_eq!(format!("{}", ArtifactKind::TopSite), "TopSite");
493 }
494
495 #[test]
496 fn artifact_kind_has_shortcut_variant() {
497 let _s = ArtifactKind::Shortcut;
498 assert_eq!(format!("{}", ArtifactKind::Shortcut), "Shortcut");
499 }
500
501 #[test]
502 fn artifact_kind_has_network_prediction_variant() {
503 let _n = ArtifactKind::NetworkPrediction;
504 assert_eq!(
505 format!("{}", ArtifactKind::NetworkPrediction),
506 "NetworkPrediction"
507 );
508 }
509
510 #[test]
511 fn artifact_kind_has_media_playback_variant() {
512 let _m = ArtifactKind::MediaPlayback;
513 assert_eq!(format!("{}", ArtifactKind::MediaPlayback), "MediaPlayback");
514 }
515
516 #[test]
517 fn forensic_meta_lookup_chrome_history() {
518 let meta = ForensicMeta::lookup("browser_chrome_history");
519 assert!(meta.is_some());
520 let meta = meta.unwrap();
521 assert_eq!(meta.artifact_id, "browser_chrome_history");
522 assert!(meta.evidence_strength.is_some());
523 }
524
525 #[test]
526 fn forensic_meta_lookup_unknown_returns_none() {
527 let meta = ForensicMeta::lookup("nonexistent_artifact_xyz");
528 assert!(meta.is_none());
529 }
530
531 #[test]
532 fn forensic_meta_all_browser_artifacts_have_profiles() {
533 let artifact_ids = [
534 "browser_chrome_history",
535 "browser_chrome_cookies",
536 "browser_chrome_downloads",
537 "browser_chrome_bookmarks",
538 "browser_chrome_extensions",
539 "browser_chrome_autofill",
540 "browser_chrome_cache",
541 "browser_chrome_session",
542 "browser_firefox_history",
543 "browser_firefox_cookies",
544 "browser_firefox_downloads",
545 "browser_safari_history",
546 ];
547
548 for id in &artifact_ids {
549 let meta = ForensicMeta::lookup(id);
550 assert!(
551 meta.is_some(),
552 "ForensicMeta::lookup({id}) should return Some"
553 );
554 }
555 }
556
557 #[test]
558 fn forensic_meta_evidence_strength_is_populated() {
559 let meta = ForensicMeta::lookup("browser_chrome_downloads").expect("should exist");
560 assert!(
561 meta.evidence_strength.is_some(),
562 "evidence_strength should be Some"
563 );
564 let strength = meta
566 .evidence_strength
567 .as_deref()
568 .expect("should have value");
569 assert!(
570 strength.contains("Strong"),
571 "Downloads should be Strong evidence, got: {strength}"
572 );
573 }
574}