1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Ask the operating system's malware scanner for a verdict on one saved file.
//!
//! [`scan_file`] runs the Windows Defender command-line scanner (`MpCmdRun.exe -Scan -ScanType 3
//! -File <path>`) with no console window and maps its exit status to a [`ScanVerdict`]. On every
//! other platform, and whenever the scanner cannot be found, started or finished in time, the
//! verdict is [`ScanVerdict::Unavailable`]. The policy stays with the app: which files are scanned
//! and what a [`ScanVerdict::Threat`] does. `Unavailable` must never be treated as clean evidence;
//! apps that block on a threat should not block on it either.
//!
//! The scanner is looked for under `%ProgramData%\Microsoft\Windows Defender\Platform\<version>\`
//! (the highest version name first), then `C:\Program Files\Windows Defender\`. The scan is bounded
//! to [`SCAN_TIMEOUT`]; a scan still running then is killed and reported `Unavailable`.
use std::path::Path;
use std::time::Duration;
/// How long one scan may run before it is abandoned.
pub const SCAN_TIMEOUT: Duration = Duration::from_secs(120);
/// What the OS scanner said about a file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScanVerdict {
/// The scanner ran and found nothing (exit status 0).
Clean,
/// The scanner ran and found a threat (exit status 2).
Threat,
/// No scanner here, it did not run, it timed out, or it reported any other status.
Unavailable,
}
/// Scan the regular file at `path` with the OS malware scanner. See the module docs.
pub fn scan_file(path: &Path) -> ScanVerdict {
imp::scan_file(path)
}
#[cfg(windows)]
mod imp {
use super::{ScanVerdict, SCAN_TIMEOUT};
use std::os::windows::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
fn locate_mpcmdrun() -> Option<PathBuf> {
let program_data = std::env::var_os("ProgramData")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
let platform = program_data
.join("Microsoft")
.join("Windows Defender")
.join("Platform");
let mut versions: Vec<PathBuf> = std::fs::read_dir(&platform)
.ok()?
.flatten()
.map(|e| e.path())
.filter(|p| p.join("MpCmdRun.exe").is_file())
.collect();
versions.sort();
if let Some(newest) = versions.pop() {
return Some(newest.join("MpCmdRun.exe"));
}
let fallback = PathBuf::from(r"C:\Program Files\Windows Defender\MpCmdRun.exe");
fallback.is_file().then_some(fallback)
}
pub fn scan_file(path: &Path) -> ScanVerdict {
let plain = crate::plain::plain_path(path.to_path_buf());
if !std::fs::metadata(&plain).is_ok_and(|m| m.is_file()) {
return ScanVerdict::Unavailable;
}
let Some(exe) = locate_mpcmdrun() else {
return ScanVerdict::Unavailable;
};
let Ok(mut child) = Command::new(exe)
.args(["-Scan", "-ScanType", "3", "-File"])
.arg(&plain)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW)
.spawn()
else {
return ScanVerdict::Unavailable;
};
let deadline = Instant::now() + SCAN_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(status)) => {
return match status.code() {
Some(0) => ScanVerdict::Clean,
Some(2) => ScanVerdict::Threat,
_ => ScanVerdict::Unavailable,
}
}
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(50));
}
Ok(None) | Err(_) => {
let _ = child.kill();
let _ = child.wait();
return ScanVerdict::Unavailable;
}
}
}
}
}
#[cfg(not(windows))]
mod imp {
use super::ScanVerdict;
use std::path::Path;
pub fn scan_file(_path: &Path) -> ScanVerdict {
ScanVerdict::Unavailable
}
}