Skip to main content

ext4fs/forensic/
timeline.rs

1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::Timestamp;
6use std::io::{Read, Seek};
7
8/// Type of filesystem event.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum EventType {
11    Created,
12    Modified,
13    Accessed,
14    Changed,
15    Deleted,
16    Mounted,
17}
18
19/// A single event in the forensic timeline.
20#[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
31/// Generate a forensic timeline from all filesystem timestamps.
32pub 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}