rightkit-fs 0.4.12

Safe state directories, atomic writes, cross-process locks, owner-only files, and stable file identity for Right Suite apps.
Documentation
//! 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
    }
}