//! Fixed PowerShell helpers over Windows OpenSSH (port of `sessions/windowsPrivilegedExec.js`). //! The command line is a short `-EncodedCommand` bootstrap; the script and one JSON argument //! object follow on stdin as base64 lines, so user input never reaches a command line. //! Windows has no sudo: scripts refuse administrative work without an elevated token and //! never try to bypass UAC. use crate::core::error::{CoreError, CoreResult}; use crate::core::ssh::session::{SessionManager, SessionRecord}; use base64::engine::general_purpose::STANDARD as B64; use base64::Engine; use once_cell::sync::Lazy; use regex::Regex; use serde_json::Value; use std::sync::Arc; use std::time::Duration; pub const BOOTSTRAP: &str = "$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';$i=[Console]::In;$c=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($i.ReadLine()));$a=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($i.ReadLine()));& ([ScriptBlock]::Create($c)) $a"; static ERR_LINE: Lazy = Lazy::new(|| Regex::new(r"^CHJ_ERR:([A-Z0-9_]{2,64}):([A-Za-z0-9+/=]*)\s*$").unwrap()); pub fn encoded_command(source: &str) -> String { let utf16: Vec = source.encode_utf16().flat_map(|u| u.to_le_bytes()).collect(); format!("powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {}", B64.encode(utf16)) } pub fn bootstrap_input(script: &str, args: &Value) -> String { format!("{}\n{}\n", B64.encode(script.as_bytes()), B64.encode(serde_json::to_string(args).unwrap_or_else(|_| "{}".into()).as_bytes())) } fn clean(text: &str, limit: usize) -> String { let replaced: String = text.chars().map(|c| if (c as u32) < 0x20 || c as u32 == 0x7f { ' ' } else { c }).collect(); replaced.split_whitespace().collect::>().join(" ").chars().take(limit).collect() } pub fn parse_windows_output(stdout: &str, stderr: &str, code: Option, prefix: &str) -> CoreResult { let lines: Vec<&str> = stdout.split('\n').map(|l| l.trim_end_matches('\r')).collect(); if let Some(line) = lines.iter().find(|l| l.starts_with("CHJ_RESULT:")) { return B64 .decode(line[11..].trim()) .ok() .and_then(|bytes| serde_json::from_slice(&bytes).ok()) .ok_or_else(|| CoreError::new(format!("{prefix}PROTOCOL_ERROR"), "The Windows helper returned unreadable data.")); } if let Some(line) = lines.iter().find(|l| l.starts_with("CHJ_ERR:")) { if let Some(caps) = ERR_LINE.captures(line) { let message = B64.decode(&caps[2]).ok().map(|b| clean(&String::from_utf8_lossy(&b), 600)).unwrap_or_default(); return Err(CoreError::new(format!("{prefix}{}", &caps[1]), if message.is_empty() { caps[1].to_string() } else { message })); } } let text = format!("{stdout}\n{stderr}"); let not_found = Regex::new(r"(?i)is not recognized as (an internal or external command|the name of a cmdlet)|powershell(\.exe)?: (command )?not found").unwrap(); if code == Some(9009) || not_found.is_match(&text) { return Err(CoreError::new(format!("{prefix}POWERSHELL_UNAVAILABLE"), "Windows PowerShell (powershell.exe) could not be started over this SSH session.")); } if Regex::new(r"(?i)language mode|ConstrainedLanguage|cannot be loaded because running scripts is disabled").unwrap().is_match(&text) { return Err(CoreError::new( format!("{prefix}POWERSHELL_RESTRICTED"), "PowerShell is restricted by policy (Constrained Language Mode or script blocking); CH-J does not bypass it.", )); } let tags = Regex::new(r"<[^>]+>").unwrap(); Err(CoreError::new( format!("{prefix}PROTOCOL_ERROR"), format!("The Windows helper did not return a result (exit {}). {}", code.map(|c| c.to_string()).unwrap_or_else(|| "null".into()), clean(&tags.replace_all(stderr, " "), 300)), )) } /// Runs a fixed PowerShell script on a Windows session, serialized per session. pub async fn run_windows_script(manager: &SessionManager, record: &Arc, script: &str, args: &Value, timeout: Duration, output_limit: usize, prefix: &str) -> CoreResult { let input = bootstrap_input(script, args); let result = manager .windows_serial(record, async { let mut channel = manager.exec_channel(record, &encoded_command(BOOTSTRAP), timeout, output_limit).await?; channel.write(input.as_bytes()).await; channel.eof().await; channel.finish().await }) .await?; parse_windows_output(&result.stdout, &result.stderr, result.code, prefix) } pub struct WindowsExecutor { pub sessions: Arc, } impl WindowsExecutor { pub async fn run(&self, session_id: &str, script: &str, args: &Value, timeout: Duration, purpose: &str, prefix: &str) -> CoreResult { let record = self.sessions.require_connected(session_id)?; let result = run_windows_script(&self.sessions, &record, script, args, timeout, 16 * 1024 * 1024, prefix).await; if let Err(error) = &result { self.sessions.logger.warn("Windows remote operation failed.", serde_json::json!({ "sessionId": session_id, "purpose": purpose, "code": error.code })); } result } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn encoded_command_is_utf16le_base64() { let command = encoded_command("a"); assert!(command.ends_with("-EncodedCommand YQA=")); assert!(encoded_command(BOOTSTRAP).len() < 8191); } #[test] fn output_parsing() { let ok = format!("noise\r\nCHJ_RESULT:{}\r\n", B64.encode(br#"{"a":1}"#)); assert_eq!(parse_windows_output(&ok, "", Some(0), "USERS_").unwrap(), json!({"a": 1})); let err = format!("CHJ_ERR:NOT_ELEVATED:{}\n", B64.encode("Run as admin")); let error = parse_windows_output(&err, "", Some(1), "USERS_").unwrap_err(); assert_eq!(error.code, "USERS_NOT_ELEVATED"); assert_eq!(error.message, "Run as admin"); assert_eq!(parse_windows_output("", "'powershell.exe' is not recognized as an internal or external command", Some(1), "X_").unwrap_err().code, "X_POWERSHELL_UNAVAILABLE"); } }