ext4fs/forensic/
timeline.rs1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::Timestamp;
6use std::io::{Read, Seek};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum EventType {
11 Created,
12 Modified,
13 Accessed,
14 Changed,
15 Deleted,
16 Mounted,
17}
18
19#[derive(Debug, Clone)]
21pub struct TimelineEvent {
22 pub timestamp: Timestamp,
23 pub event_type: EventType,
24 pub inode: u64,
25 pub path: Option<String>,
26 pub size: u64,
27 pub uid: u32,
28 pub gid: u32,
29}
30
31pub fn generate_timeline<R: Read + Seek>(
33 reader: &mut InodeReader<R>,
34) -> Result<Vec<TimelineEvent>> {
35 let mut events = Vec::new();
36
37 let sb = reader.block_reader().superblock();
38 if sb.mount_time != 0 {
39 events.push(TimelineEvent {
40 timestamp: Timestamp {
41 seconds: i64::from(sb.mount_time),
42 nanoseconds: 0,
43 },
44 event_type: EventType::Mounted,
45 inode: 0,
46 path: None,
47 size: 0,
48 uid: 0,
49 gid: 0,
50 });
51 }
52
53 let all_inodes = reader.iter_all_inodes()?;
54 for (ino, inode) in &all_inodes {
55 let base = |ts: &Timestamp, event_type: EventType| -> Option<TimelineEvent> {
56 if ts.seconds == 0 {
57 return None;
58 }
59 Some(TimelineEvent {
60 timestamp: *ts,
61 event_type,
62 inode: *ino,
63 path: None,
64 size: inode.size,
65 uid: inode.uid,
66 gid: inode.gid,
67 })
68 };
69
70 if let Some(e) = base(&inode.crtime, EventType::Created) {
71 events.push(e);
72 }
73 if let Some(e) = base(&inode.mtime, EventType::Modified) {
74 events.push(e);
75 }
76 if let Some(e) = base(&inode.atime, EventType::Accessed) {
77 events.push(e);
78 }
79 if let Some(e) = base(&inode.ctime, EventType::Changed) {
80 events.push(e);
81 }
82 if inode.dtime != 0 {
83 events.push(TimelineEvent {
84 timestamp: Timestamp {
85 seconds: i64::from(inode.dtime),
86 nanoseconds: 0,
87 },
88 event_type: EventType::Deleted,
89 inode: *ino,
90 path: None,
91 size: inode.size,
92 uid: inode.uid,
93 gid: inode.gid,
94 });
95 }
96 }
97
98 events.sort_by_key(|e| e.timestamp);
99 Ok(events)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::block::BlockReader;
106 use crate::inode::InodeReader;
107 use std::io::Cursor;
108
109 fn open_minimal() -> Option<InodeReader<Cursor<Vec<u8>>>> {
110 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
111 let data = std::fs::read(path).ok()?;
112 let br = BlockReader::open(Cursor::new(data)).ok()?;
113 Some(InodeReader::new(br))
114 }
115
116 #[test]
117 fn generate_timeline_from_minimal() {
118 let mut reader = if let Some(r) = open_minimal() {
119 r
120 } else {
121 eprintln!("skip: minimal.img not found");
122 return;
123 };
124 let events = generate_timeline(&mut reader).unwrap();
125 assert!(!events.is_empty());
126 for window in events.windows(2) {
127 assert!(window[0].timestamp <= window[1].timestamp);
128 }
129 }
130
131 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
132 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
133 let data = std::fs::read(path).ok()?;
134 let br = BlockReader::open(Cursor::new(data)).ok()?;
135 Some(InodeReader::new(br))
136 }
137
138 #[test]
139 fn forensic_timeline_contains_deletion_events() {
140 let mut reader = if let Some(r) = open_forensic() {
141 r
142 } else {
143 eprintln!("skip: forensic.img not found");
144 return;
145 };
146 let events = generate_timeline(&mut reader).unwrap();
147 let deleted: Vec<&TimelineEvent> = events
148 .iter()
149 .filter(|e| e.event_type == EventType::Deleted)
150 .collect();
151 assert!(!deleted.is_empty(), "expected at least one Deleted event");
152 let deleted_inodes: Vec<u64> = deleted.iter().map(|e| e.inode).collect();
153 assert!(
154 deleted_inodes.contains(&21),
155 "expected Deleted event for inode 21"
156 );
157 assert!(
158 deleted_inodes.contains(&22),
159 "expected Deleted event for inode 22"
160 );
161 }
162
163 #[test]
164 fn forensic_timeline_contains_all_event_types() {
165 let mut reader = if let Some(r) = open_forensic() {
166 r
167 } else {
168 eprintln!("skip: forensic.img not found");
169 return;
170 };
171 let events = generate_timeline(&mut reader).unwrap();
172 let has = |et: EventType| events.iter().any(|e| e.event_type == et);
173 assert!(has(EventType::Created), "missing Created events");
174 assert!(has(EventType::Modified), "missing Modified events");
175 assert!(has(EventType::Accessed), "missing Accessed events");
176 assert!(has(EventType::Changed), "missing Changed events");
177 assert!(has(EventType::Deleted), "missing Deleted events");
178 }
179
180 #[test]
181 fn forensic_timeline_is_sorted() {
182 let mut reader = if let Some(r) = open_forensic() {
183 r
184 } else {
185 eprintln!("skip: forensic.img not found");
186 return;
187 };
188 let events = generate_timeline(&mut reader).unwrap();
189 assert!(events.len() > 1, "need multiple events to verify sorting");
190 for window in events.windows(2) {
191 assert!(
192 window[0].timestamp <= window[1].timestamp,
193 "events not sorted: {:?} > {:?}",
194 window[0].timestamp,
195 window[1].timestamp,
196 );
197 }
198 }
199}