use crate::token::world_sid; use crate::winutil::to_wide; use anyhow::Result; use std::collections::HashSet; use std::ffi::c_void; use std::path::Path; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; use windows_sys::Win32::Foundation::LocalFree; use windows_sys::Win32::Foundation::ERROR_SUCCESS; use windows_sys::Win32::Foundation::HLOCAL; use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW; use windows_sys::Win32::Security::Authorization::GetSecurityInfo; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Storage::FileSystem::CreateFileW; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE; use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ; use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE; use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING; use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA; use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA; use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA; use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES; const GENERIC_ALL_MASK: u32 = 0x1000_0000; const GENERIC_WRITE_MASK: u32 = 0x4000_0000; use windows_sys::Win32::Security::ACL; use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION; use windows_sys::Win32::Security::ACL_SIZE_INFORMATION; use windows_sys::Win32::Security::AclSizeInformation; use windows_sys::Win32::Security::GetAclInformation; use windows_sys::Win32::Security::GetAce; use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE; use windows_sys::Win32::Security::ACE_HEADER; use windows_sys::Win32::Security::EqualSid; // Preflight scan limits const MAX_ITEMS_PER_DIR: i32 = 1000; const AUDIT_TIME_LIMIT_SECS: i64 = 2; const MAX_CHECKED_LIMIT: i32 = 50000; // Case-insensitive suffixes (normalized to forward slashes) to skip during one-level child scan const SKIP_DIR_SUFFIXES: &[&str] = &[ "/windows/installer", "/windows/registration", "/programdata", ]; fn normalize_path_key(p: &Path) -> String { let n = dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()); n.to_string_lossy().replace('\\', "/").to_ascii_lowercase() } fn unique_push(set: &mut HashSet, out: &mut Vec, p: PathBuf) { if let Ok(abs) = p.canonicalize() { if set.insert(abs.clone()) { out.push(abs); } } } fn gather_candidates(cwd: &Path, env: &std::collections::HashMap) -> Vec { let mut set: HashSet = HashSet::new(); let mut out: Vec = Vec::new(); // 1) CWD first (so immediate children get scanned early) unique_push(&mut set, &mut out, cwd.to_path_buf()); // 2) TEMP/TMP next (often small, quick to scan) for k in ["TEMP", "TMP"] { if let Some(v) = env.get(k).cloned().or_else(|| std::env::var(k).ok()) { unique_push(&mut set, &mut out, PathBuf::from(v)); } } // 3) User roots if let Some(up) = std::env::var_os("USERPROFILE") { unique_push(&mut set, &mut out, PathBuf::from(up)); } if let Some(pubp) = std::env::var_os("PUBLIC") { unique_push(&mut set, &mut out, PathBuf::from(pubp)); } // 4) PATH entries (best-effort) if let Some(path) = env .get("PATH") .cloned() .or_else(|| std::env::var("PATH").ok()) { for part in path.split(std::path::MAIN_SEPARATOR) { if !part.is_empty() { unique_push(&mut set, &mut out, PathBuf::from(part)); } } } // 5) Core system roots last for p in [PathBuf::from("C:/"), PathBuf::from("C:/Windows")] { unique_push(&mut set, &mut out, p); } out } unsafe fn path_has_world_write_allow(path: &Path) -> Result { // Prefer handle-based query (often faster than name-based), fallback to name-based on error let mut p_sd: *mut c_void = std::ptr::null_mut(); let mut p_dacl: *mut ACL = std::ptr::null_mut(); let mut try_named = false; let wpath = to_wide(path); let h = CreateFileW( wpath.as_ptr(), 0x00020000, // READ_CONTROL FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, std::ptr::null_mut(), OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0, ); if h == INVALID_HANDLE_VALUE { try_named = true; } else { let code = GetSecurityInfo( h, 1, // SE_FILE_OBJECT DACL_SECURITY_INFORMATION, std::ptr::null_mut(), std::ptr::null_mut(), &mut p_dacl, std::ptr::null_mut(), &mut p_sd, ); CloseHandle(h); if code != ERROR_SUCCESS { try_named = true; if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); p_sd = std::ptr::null_mut(); p_dacl = std::ptr::null_mut(); } } } if try_named { let code = GetNamedSecurityInfoW( wpath.as_ptr(), 1, DACL_SECURITY_INFORMATION, std::ptr::null_mut(), std::ptr::null_mut(), &mut p_dacl, std::ptr::null_mut(), &mut p_sd, ); if code != ERROR_SUCCESS { if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); } return Ok(false); } } let mut world = world_sid()?; let psid_world = world.as_mut_ptr() as *mut c_void; // Very fast mask-based check for world-writable grants (includes GENERIC_*). if !dacl_quick_world_write_mask_allows(p_dacl, psid_world) { if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); } return Ok(false); } // Quick detector flagged a write grant for Everyone: treat as writable. let has = true; if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); } Ok(has) } pub fn audit_everyone_writable( cwd: &Path, env: &std::collections::HashMap, logs_base_dir: Option<&Path>, ) -> Result> { let start = Instant::now(); let mut flagged: Vec = Vec::new(); let mut seen: HashSet = HashSet::new(); let mut checked = 0usize; // Fast path: check CWD immediate children first so workspace issues are caught early. if let Ok(read) = std::fs::read_dir(cwd) { for ent in read.flatten().take(MAX_ITEMS_PER_DIR as usize) { if start.elapsed() > Duration::from_secs(AUDIT_TIME_LIMIT_SECS as u64) || checked > MAX_CHECKED_LIMIT as usize { break; } let ft = match ent.file_type() { Ok(ft) => ft, Err(_) => continue, }; if ft.is_symlink() || !ft.is_dir() { continue; } let p = ent.path(); checked += 1; let has = unsafe { path_has_world_write_allow(&p)? }; if has { let key = normalize_path_key(&p); if seen.insert(key) { flagged.push(p); } } } } // Continue with broader candidate sweep let candidates = gather_candidates(cwd, env); for root in candidates { if start.elapsed() > Duration::from_secs(AUDIT_TIME_LIMIT_SECS as u64) || checked > MAX_CHECKED_LIMIT as usize { break; } checked += 1; let has_root = unsafe { path_has_world_write_allow(&root)? }; if has_root { let key = normalize_path_key(&root); if seen.insert(key) { flagged.push(root.clone()); } } // one level down best-effort if let Ok(read) = std::fs::read_dir(&root) { for ent in read.flatten().take(MAX_ITEMS_PER_DIR as usize) { let p = ent.path(); if start.elapsed() > Duration::from_secs(AUDIT_TIME_LIMIT_SECS as u64) || checked > MAX_CHECKED_LIMIT as usize { break; } // Skip reparse points (symlinks/junctions) to avoid auditing link ACLs let ft = match ent.file_type() { Ok(ft) => ft, Err(_) => continue, }; if ft.is_symlink() { continue; } // Skip noisy/irrelevant Windows system subdirectories let pl = p.to_string_lossy().to_ascii_lowercase(); let norm = pl.replace('\\', "/"); if SKIP_DIR_SUFFIXES.iter().any(|s| norm.ends_with(s)) { continue; } if ft.is_dir() { checked += 1; let has_child = unsafe { path_has_world_write_allow(&p)? }; if has_child { let key = normalize_path_key(&p); if seen.insert(key) { flagged.push(p); } } } } } } let elapsed_ms = start.elapsed().as_millis(); if !flagged.is_empty() { let mut list = String::new(); for p in &flagged { list.push_str(&format!("\n - {}", p.display())); } crate::logging::log_note( &format!( "AUDIT: world-writable scan FAILED; checked={checked}; duration_ms={elapsed_ms}; flagged:{}", list ), logs_base_dir, ); return Ok(flagged); } // Log success once if nothing flagged crate::logging::log_note( &format!( "AUDIT: world-writable scan OK; checked={checked}; duration_ms={elapsed_ms}" ), logs_base_dir, ); Ok(Vec::new()) } // Fast mask-based check: does the DACL contain any ACCESS_ALLOWED ACE for // Everyone that includes generic or specific write bits? Skips inherit-only // ACEs (do not apply to the current object). unsafe fn dacl_quick_world_write_mask_allows(p_dacl: *mut ACL, psid_world: *mut c_void) -> bool { if p_dacl.is_null() { return false; } const INHERIT_ONLY_ACE: u8 = 0x08; let mut info: ACL_SIZE_INFORMATION = std::mem::zeroed(); let ok = GetAclInformation( p_dacl as *const ACL, &mut info as *mut _ as *mut c_void, std::mem::size_of::() as u32, AclSizeInformation, ); if ok == 0 { return false; } for i in 0..(info.AceCount as usize) { let mut p_ace: *mut c_void = std::ptr::null_mut(); if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 { continue; } let hdr = &*(p_ace as *const ACE_HEADER); if hdr.AceType != 0 { // ACCESS_ALLOWED_ACE_TYPE continue; } if (hdr.AceFlags & INHERIT_ONLY_ACE) != 0 { continue; } let base = p_ace as usize; let sid_ptr = (base + std::mem::size_of::() + std::mem::size_of::()) as *mut c_void; // skip header + mask if EqualSid(sid_ptr, psid_world) != 0 { let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE); let mask = ace.Mask; let writey = FILE_GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | GENERIC_WRITE_MASK | GENERIC_ALL_MASK; if (mask & writey) != 0 { return true; } } } false }