1#![forbid(unsafe_code)]
17
18use forensicnomicon::report::{Category, Finding, Observation, Severity, Source, SubjectRef};
19use prefetch_core::{PrefetchError, PrefetchInfo};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ExecutionRecord {
24 pub executable: String,
26 pub run_count: u32,
28 pub last_run_filetimes: Vec<i64>,
30 pub image_path: Option<String>,
33 pub volume_serial: Option<u32>,
35 pub loaded_file_count: usize,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum PrefetchAnomaly {
44 SystemBinaryRelocated {
47 name: String,
49 image_path: String,
51 },
52 SuspiciousExecutionPath {
55 executable: String,
57 image_path: String,
59 },
60}
61
62#[must_use]
64pub fn execution_record(info: &PrefetchInfo) -> ExecutionRecord {
65 ExecutionRecord {
66 executable: info.executable.clone(),
67 run_count: info.run_count,
68 last_run_filetimes: info.last_run_times.clone(),
69 image_path: image_path_of(info),
70 volume_serial: info.volumes.first().map(|v| v.serial),
71 loaded_file_count: info.filenames.len(),
72 }
73}
74
75fn image_path_of(info: &PrefetchInfo) -> Option<String> {
78 let exe = info.executable.to_uppercase();
79 info.filenames
80 .iter()
81 .find(|f| f.to_uppercase().ends_with(&exe))
82 .cloned()
83}
84
85#[must_use]
88pub fn audit(info: &PrefetchInfo) -> Vec<PrefetchAnomaly> {
89 let mut out = Vec::new();
90 let Some(image_path) = image_path_of(info) else {
91 return out;
92 };
93 let upper = image_path.to_uppercase();
94
95 let in_system32 = upper.contains(r"\SYSTEM32\") || upper.contains(r"\SYSWOW64\");
98 if forensicnomicon::processes::is_system32_binary(&info.executable) && !in_system32 {
99 out.push(PrefetchAnomaly::SystemBinaryRelocated {
100 name: info.executable.to_uppercase(),
101 image_path: image_path.clone(),
102 });
103 }
104
105 if forensicnomicon::heuristics::paths::is_suspicious_exec_path(&image_path) {
106 out.push(PrefetchAnomaly::SuspiciousExecutionPath {
107 executable: info.executable.clone(),
108 image_path,
109 });
110 }
111 out
112}
113
114pub fn audit_bytes(
118 file_bytes: &[u8],
119) -> Result<(ExecutionRecord, Vec<PrefetchAnomaly>), PrefetchError> {
120 let info = prefetch_core::parse(file_bytes)?;
121 Ok((execution_record(&info), audit(&info)))
122}
123
124impl Observation for PrefetchAnomaly {
125 fn severity(&self) -> Option<Severity> {
126 Some(match self {
127 PrefetchAnomaly::SystemBinaryRelocated { .. } => Severity::High,
128 PrefetchAnomaly::SuspiciousExecutionPath { .. } => Severity::Medium,
129 })
130 }
131
132 fn category(&self) -> Category {
133 match self {
134 PrefetchAnomaly::SystemBinaryRelocated { .. } => Category::Concealment,
135 PrefetchAnomaly::SuspiciousExecutionPath { .. } => Category::Threat,
136 }
137 }
138
139 fn code(&self) -> &'static str {
140 match self {
141 PrefetchAnomaly::SystemBinaryRelocated { .. } => "PREFETCH-SYSTEM-BINARY-RELOCATED",
142 PrefetchAnomaly::SuspiciousExecutionPath { .. } => "PREFETCH-SUSPICIOUS-EXEC-PATH",
143 }
144 }
145
146 fn note(&self) -> String {
147 match self {
148 PrefetchAnomaly::SystemBinaryRelocated { name, image_path } => format!(
149 "{name} is a Windows system binary, but prefetch traced its image load \
150 from {image_path} — consistent with masquerading."
151 ),
152 PrefetchAnomaly::SuspiciousExecutionPath {
153 executable,
154 image_path,
155 } => format!(
156 "{executable} executed from {image_path}, a directory commonly used to \
157 stage malware — consistent with suspicious execution."
158 ),
159 }
160 }
161
162 fn mitre(&self) -> &'static [&'static str] {
163 match self {
164 PrefetchAnomaly::SystemBinaryRelocated { .. } => &["T1036.005"],
165 PrefetchAnomaly::SuspiciousExecutionPath { .. } => &["T1204"],
166 }
167 }
168
169 fn subjects(&self) -> Vec<SubjectRef> {
170 let (name, path) = match self {
171 PrefetchAnomaly::SystemBinaryRelocated { name, image_path } => (name, image_path),
172 PrefetchAnomaly::SuspiciousExecutionPath {
173 executable,
174 image_path,
175 } => (executable, image_path),
176 };
177 vec![SubjectRef {
178 scheme: "filesystem".to_string(),
179 kind: "executable".to_string(),
180 id: path.clone(),
181 label: Some(name.clone()),
182 }]
183 }
184}
185
186#[must_use]
188pub fn to_finding(anomaly: &PrefetchAnomaly, scope: impl Into<String>) -> Finding {
189 anomaly.to_finding(Source {
190 analyzer: "prefetch-forensic".to_string(),
191 scope: scope.into(),
192 version: Some(env!("CARGO_PKG_VERSION").to_string()),
193 })
194}
195
196#[cfg(test)]
197#[allow(clippy::unwrap_used, clippy::expect_used)]
198mod tests {
199 use super::*;
200
201 const COREUPDATER: &[u8] = include_bytes!("../../tests/data/COREUPDATER.EXE-157C54BB.pf");
202
203 #[test]
207 fn coreupdater_yields_execution_evidence_and_no_fp() {
208 let (rec, anomalies) = audit_bytes(COREUPDATER).unwrap();
209 assert_eq!(rec.executable, "COREUPDATER.EXE");
210 assert_eq!(rec.run_count, 1);
211 assert_eq!(rec.last_run_filetimes, vec![132_449_604_494_103_203]);
212 assert_eq!(rec.volume_serial, Some(0xB0E0_E8FF));
213 assert_eq!(rec.loaded_file_count, 51);
214 assert!(rec
215 .image_path
216 .unwrap()
217 .ends_with(r"\SYSTEM32\COREUPDATER.EXE"));
218 assert!(anomalies.is_empty());
220 }
221
222 fn info_with(exe: &str, image_path: &str) -> PrefetchInfo {
223 PrefetchInfo {
224 version: 30,
225 executable: exe.to_string(),
226 run_count: 2,
227 last_run_times: vec![1],
228 volumes: Vec::new(),
229 filenames: vec![image_path.to_string()],
230 }
231 }
232
233 #[test]
234 fn masqueraded_system_binary_is_high() {
235 let info = info_with("SVCHOST.EXE", r"\VOLUME{x}\WINDOWS\TEMP\SVCHOST.EXE");
236 let anomalies = audit(&info);
237 assert!(anomalies
239 .iter()
240 .any(|a| matches!(a, PrefetchAnomaly::SystemBinaryRelocated { .. })));
241 let f = to_finding(
242 anomalies
243 .iter()
244 .find(|a| matches!(a, PrefetchAnomaly::SystemBinaryRelocated { .. }))
245 .unwrap(),
246 "Desktop",
247 );
248 assert_eq!(f.severity, Some(Severity::High));
249 assert_eq!(f.code, "PREFETCH-SYSTEM-BINARY-RELOCATED");
250 assert_eq!(f.category, Category::Concealment);
251 }
252
253 #[test]
254 fn legit_system_binary_in_system32_is_clean() {
255 let info = info_with("SVCHOST.EXE", r"\VOLUME{x}\WINDOWS\SYSTEM32\SVCHOST.EXE");
256 assert!(audit(&info).is_empty());
257 }
258
259 #[test]
260 fn execution_from_downloads_is_medium_threat() {
261 let info = info_with("INVOICE.EXE", r"\VOLUME{x}\USERS\BOB\DOWNLOADS\INVOICE.EXE");
262 let anomalies = audit(&info);
263 let a = anomalies
264 .iter()
265 .find(|a| matches!(a, PrefetchAnomaly::SuspiciousExecutionPath { .. }))
266 .expect("downloads path should be flagged");
267 let f = to_finding(a, "Desktop");
268 assert_eq!(f.severity, Some(Severity::Medium));
269 assert_eq!(f.category, Category::Threat);
270 assert_eq!(f.code, "PREFETCH-SUSPICIOUS-EXEC-PATH");
271 assert!(f.note.contains("INVOICE.EXE"));
272 }
273
274 #[test]
275 fn no_image_path_yields_no_anomaly() {
276 let info = info_with("FOO.EXE", r"\VOLUME{x}\WINDOWS\SYSTEM32\NTDLL.DLL");
278 assert!(audit(&info).is_empty());
279 }
280}