//! Remote OS detection from system facts (port of `sessions/platformDetect.js`): //! `uname -s` for POSIX, otherwise an encoded PowerShell probe; never prompts or host names. use crate::core::error::CoreResult; use crate::core::ssh::session::{SessionManager, SessionRecord}; use crate::core::ssh::windows_exec::{encoded_command, parse_windows_output}; use once_cell::sync::Lazy; use regex::Regex; use serde_json::{json, Value}; use std::sync::Arc; use std::time::Duration; static POSIX_KERNELS: Lazy = Lazy::new(|| Regex::new(r"^(Linux|Darwin|FreeBSD|OpenBSD|NetBSD|DragonFly|SunOS|AIX)\b").unwrap()); pub const WINDOWS_PROBE: &str = r#"$ProgressPreference='SilentlyContinue' try { # Registry first: standard accounts on an SSH network logon are usually denied WMI/CIM access. $nt = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue $shell = (Get-ItemProperty 'HKLM:\SOFTWARE\OpenSSH' -ErrorAction SilentlyContinue).DefaultShell $caption = [string]$nt.ProductName; $build = [string]$nt.CurrentBuildNumber if ($caption -match '^Windows 10' -and [int]("0$build") -ge 22000) { $caption = $caption -replace '^Windows 10', 'Windows 11' } $type = [string](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\ProductOptions' -ErrorAction SilentlyContinue).ProductType $productType = @{ 'WinNT' = 1; 'LanmanNT' = 2; 'ServerNT' = 3 }[$type] $version = [Environment]::OSVersion.Version.ToString() try { $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop; $caption = $os.Caption; $version = $os.Version; $productType = [int]$os.ProductType } catch {} $value = [ordered]@{ caption = $caption; version = $version; build = $build; productType = $productType installationType = [string]$nt.InstallationType; defaultShell = [string]$shell; psVersion = $PSVersionTable.PSVersion.ToString() is64BitProcess = [Environment]::Is64BitProcess; languageMode = [string]$ExecutionContext.SessionState.LanguageMode localAccounts = [bool](Get-Command Get-LocalUser -ErrorAction SilentlyContinue); pwsh = [string](Get-Command pwsh.exe -ErrorAction SilentlyContinue).Source } [Console]::Out.WriteLine('CHJ_RESULT:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($value | ConvertTo-Json -Compress)))) } catch { [Console]::Out.WriteLine('CHJ_ERR:PLATFORM_PROBE_FAILED:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_.Exception.Message))) }"#; pub fn shell_name(default_shell: &str) -> String { let value = default_shell.to_lowercase(); if value.is_empty() { return "cmd.exe".into(); } for (pattern, name) in [(r"pwsh(\.exe)?$", "pwsh"), (r"powershell(\.exe)?$", "powershell"), (r"cmd(\.exe)?$", "cmd.exe"), (r"bash(\.exe)?$", "bash")] { if Regex::new(pattern).unwrap().is_match(&value) { return name.into(); } } value.rsplit(['\\', '/']).next().unwrap_or("").to_string() } pub async fn detect_platform(manager: &SessionManager, record: &Arc, refresh: bool) -> CoreResult { if !refresh { if let Some(platform) = record.platform.lock().clone() { return Ok(platform); } } let uname = match manager.exec_channel(record, "uname -s", Duration::from_secs(15), 512 * 1024).await { Ok(mut channel) => { channel.eof().await; channel.finish().await.ok() } Err(error) if error.code == "SESSION_NOT_CONNECTED" => return Err(error), Err(_) => None, }; let kernel = uname.as_ref().map(|r| r.stdout.trim().lines().next().unwrap_or("").to_string()).unwrap_or_default(); if uname.as_ref().map(|r| r.code == Some(0)).unwrap_or(false) && POSIX_KERNELS.is_match(&kernel) { let platform = json!({ "os": if kernel.starts_with("Linux") { "linux" } else { "posix" }, "kernel": kernel, "detectedBy": "uname" }); *record.platform.lock() = Some(platform.clone()); return Ok(platform); } let mut channel = manager.exec_channel(record, &encoded_command(WINDOWS_PROBE), Duration::from_secs(60), 512 * 1024).await?; channel.eof().await; let probe = channel.finish().await?; let platform = match parse_windows_output(&probe.stdout, &probe.stderr, probe.code, "PLATFORM_") { Err(error) => json!({ "os": "unknown", "kernel": kernel, "detectedBy": "none", "reason": error.message }), Ok(info) => { let text = |k: &str| info.get(k).map(|v| v.as_str().map(str::to_string).unwrap_or_else(|| if v.is_null() { String::new() } else { v.to_string() })).unwrap_or_default(); json!({ "os": "windows", "detectedBy": "Win32_OperatingSystem", "caption": text("caption"), "version": text("version"), "build": text("build"), "server": info.get("productType").and_then(Value::as_f64) != Some(1.0), "serverCore": Regex::new(r"(?i)server core").unwrap().is_match(&text("installationType")), "installationType": text("installationType"), "defaultShell": shell_name(&text("defaultShell")), "defaultShellPath": text("defaultShell"), "powershell": text("psVersion"), "is64BitProcess": info.get("is64BitProcess") == Some(&Value::Bool(true)), "languageMode": text("languageMode"), "localAccounts": info.get("localAccounts") == Some(&Value::Bool(true)), "pwsh": text("pwsh"), "unameOutput": kernel }) } }; *record.platform.lock() = Some(platform.clone()); Ok(platform) } #[cfg(test)] mod tests { use super::*; #[test] fn shells() { assert_eq!(shell_name(""), "cmd.exe"); assert_eq!(shell_name("C:\\Program Files\\PowerShell\\7\\pwsh.exe"), "pwsh"); assert_eq!(shell_name("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), "powershell"); assert_eq!(shell_name("C:\\tools\\fish.exe"), "fish.exe"); } }