use std::path::Path;
use super::evidence::{HostProfile, bitlocker_record_valid, classify};
use crate::space::command::{self, Tool};
use crate::space::paths::Paths;
const BITLOCKER_QUERY: &str = "$ErrorActionPreference=\"Stop\"; $disk=Join-Path $env:LOCALAPPDATA \"Docker\\wsl\\data\\docker_data.vhdx\"; if (-not (Test-Path -LiteralPath $disk -PathType Leaf)) { throw \"missing Docker data disk\" }; $root=[System.IO.Path]::GetPathRoot($disk); $volume=Get-BitLockerVolume -MountPoint $root; if ($null -eq $volume) { throw \"missing BitLocker volume\" }; \"shimpz-bitlocker-v1|$($volume.VolumeStatus)|$($volume.ProtectionStatus)|$($volume.EncryptionPercentage)\"";
pub(crate) fn detect() -> Result<HostProfile, String> {
let microsoft_kernel = std::fs::read_to_string("/proc/version")
.is_ok_and(|value| value.to_ascii_lowercase().contains("microsoft"));
let wsl_interop = Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists();
let pid_one = std::fs::read_to_string("/proc/1/comm").unwrap_or_default();
classify(
std::env::consts::OS,
std::env::consts::ARCH,
microsoft_kernel,
wsl_interop,
&pid_one,
)
}
pub(crate) fn verify(profile: HostProfile, paths: &Paths) -> Result<(), String> {
match profile {
HostProfile::Linux => Err("native Linux requires the dedicated LUKS admission".into()),
HostProfile::MacOs => verify_macos(paths),
HostProfile::Wsl => verify_wsl(),
}
}
fn verify_macos(paths: &Paths) -> Result<(), String> {
verify_macos_with(paths, || {
command::status(Tool::FileVault, ["isactive"]).map(|status| status.success())
})
}
fn verify_macos_with<F>(paths: &Paths, filevault_active: F) -> Result<(), String>
where
F: FnOnce() -> Result<bool, String>,
{
if !filevault_active()? {
return Err("FileVault is not active".into());
}
verify_macos_disk(paths)
}
fn verify_macos_disk(paths: &Paths) -> Result<(), String> {
let disk = paths
.home
.parent()
.ok_or_else(|| "the user home is invalid".to_owned())?
.join("Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw");
let disk_link = disk
.symlink_metadata()
.map_err(|_| "Docker Desktop's default Docker.raw data disk is missing")?;
if disk_link.file_type().is_symlink() || !disk_link.is_file() {
return Err("Docker Desktop's data disk must be the supported default regular file".into());
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let startup = std::fs::metadata("/System/Volumes/Data")
.map_err(|_| "the FileVault startup data filesystem is unavailable")?;
let docker = disk
.metadata()
.map_err(|_| "Docker Desktop's data disk is unavailable")?;
if startup.dev() != docker.dev() {
return Err("Docker Desktop's data disk is outside the FileVault filesystem".into());
}
}
Ok(())
}
fn verify_wsl() -> Result<(), String> {
let record = command::output(
Tool::PowerShell,
["-NoProfile", "-NonInteractive", "-Command", BITLOCKER_QUERY],
)?;
if bitlocker_record_valid(&record) {
Ok(())
} else {
Err(
"BitLocker does not fully encrypt and protect Docker Desktop's default WSL data disk"
.into(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn production_query_is_fixed_and_bounded_to_the_default_disk() {
assert!(BITLOCKER_QUERY.contains("Docker\\wsl\\data\\docker_data.vhdx"));
assert!(BITLOCKER_QUERY.contains("Get-BitLockerVolume -MountPoint $root"));
assert!(!BITLOCKER_QUERY.contains("Invoke-Expression"));
assert!(!BITLOCKER_QUERY.contains("SHIMPZ"));
}
#[test]
fn macos_admission_denies_inactive_filevault_before_disk_inspection() {
let home = tempfile::tempdir().unwrap();
let paths = Paths::under(home.path()).unwrap();
assert_eq!(
verify_macos_with(&paths, || Ok(false)),
Err("FileVault is not active".to_owned())
);
}
#[cfg(target_os = "macos")]
#[test]
#[ignore = "requires the native macOS filesystem layout"]
fn native_macos_proves_the_exact_docker_disk_filesystem() {
let home = tempfile::tempdir().unwrap();
let paths = Paths::under(home.path()).unwrap();
let disk = home
.path()
.join("Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw");
std::fs::create_dir_all(disk.parent().unwrap()).unwrap();
std::fs::write(&disk, []).unwrap();
verify_macos_with(&paths, || Ok(true)).unwrap();
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires native Windows PowerShell and the BitLocker provider"]
fn native_windows_executes_the_exact_bitlocker_query_and_parser() {
let local = std::env::var_os("LOCALAPPDATA").expect("LOCALAPPDATA is required");
let disk = Path::new(&local).join("Docker/wsl/data/docker_data.vhdx");
std::fs::create_dir_all(disk.parent().unwrap()).unwrap();
if !disk.exists() {
std::fs::write(&disk, []).unwrap();
}
let result = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", BITLOCKER_QUERY])
.output()
.unwrap();
assert!(result.status.success());
let record = String::from_utf8(result.stdout).unwrap();
let fields: Vec<_> = record.trim().split('|').collect();
assert_eq!(fields.len(), 4);
assert_eq!(fields[0], "shimpz-bitlocker-v1");
let expected = fields[1] == "FullyEncrypted" && fields[2] == "On" && fields[3] == "100";
assert_eq!(bitlocker_record_valid(&record), expected);
}
}