From a1b2902f0209a0aab481398375002734a7c68ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20Valero?= Date: Mon, 13 Jul 2026 17:28:46 +0200 Subject: [PATCH] fix: resolve security issues, deadlocks, and clean up dead code - Resolved command injection risks in PowerShellEngine and MainViewModel. - Fixed API key leak in GeminiProvider by using request headers. - Fixed potential deadlocks in stream reading in CommandRepair and PowerShellEngine. - Removed dead code (IColector, DiagnosticContext, unused commands/properties). - Added settings.json to .gitignore to prevent local key leakage. - Converted ChatMessages, Modulos, and Problemas collections to ObservableCollection. --- .gitignore | 1 + .../ViewModels/MainViewModel.cs | 94 ++++++++++--------- src/SupportAI.Collectors.Windows/IColector.cs | 10 -- .../PowerShellEngine.cs | 46 ++++++--- .../Models/DiagnosticContext.cs | 12 --- src/SupportAI.Core/Models/DiagnosticEngine.cs | 4 +- src/SupportAI.Core/Models/HardwareInfo.cs | 8 +- src/SupportAI.Ia/GeminiProvider.cs | 31 +++--- src/SupportAI.Ia/LlmService.cs | 2 +- src/SupportAI.Ia/PrivacyFilter.cs | 6 +- src/SupportAI.Ia/RulesProvider.cs | 2 +- src/SupportAI.Repairs/CommandRepair.cs | 21 ++++- 12 files changed, 132 insertions(+), 105 deletions(-) delete mode 100644 src/SupportAI.Collectors.Windows/IColector.cs delete mode 100644 src/SupportAI.Core/Models/DiagnosticContext.cs diff --git a/.gitignore b/.gitignore index 9f2c456..39832c3 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ tools/*.exe .*_KEY *_key.txt keys/ +settings.json diff --git a/src/SupportAI.App.Wpf/ViewModels/MainViewModel.cs b/src/SupportAI.App.Wpf/ViewModels/MainViewModel.cs index b7d9bbf..4bf2746 100644 --- a/src/SupportAI.App.Wpf/ViewModels/MainViewModel.cs +++ b/src/SupportAI.App.Wpf/ViewModels/MainViewModel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -38,10 +39,7 @@ public MainViewModel() ExportPdfCommand = new AsyncRelayCommand(async _ => await ExportPdfAsync()); AnalyzeWithIaCommand = new AsyncRelayCommand(async _ => await AnalyzeWithIaAsync()); RepairCommand = new RelayCommand(ExecuteRepair); - CancelRepairCommand = new RelayCommand(_ => CancelRepair()); - DownloadModelCommand = new RelayCommand(_ => DownloadModel()); AbrirAccionCommand = new RelayCommand(AbrirAccion); - ToggleExpandirCommand = new RelayCommand(ToggleExpandir); IniciarServicioCommand = new AsyncRelayCommand(async param => await IniciarServicio(param)); OpenSettingsCommand = new RelayCommand(_ => AbrirSettings()); DescargarModeloCommand = new AsyncRelayCommand(async _ => await DescargarModeloAsync()); @@ -68,10 +66,7 @@ public MainViewModel() public ICommand ExportPdfCommand { get; } public ICommand AnalyzeWithIaCommand { get; } public ICommand RepairCommand { get; } - public ICommand CancelRepairCommand { get; } - public ICommand DownloadModelCommand { get; } public ICommand AbrirAccionCommand { get; } - public ICommand ToggleExpandirCommand { get; } public ICommand IniciarServicioCommand { get; } public ICommand OpenSettingsCommand { get; } public ICommand DescargarModeloCommand { get; } @@ -93,16 +88,26 @@ private async Task IniciarServicio(object? param) { if (param is not string nombreCorto) return; + if (!System.Text.RegularExpressions.Regex.IsMatch(nombreCorto, @"^[a-zA-Z0-9._\- ]+$")) + { + StatusText = "❌ Nombre de servicio inválido."; + return; + } + var confirm = MessageBox.Show( $"Iniciar el servicio '{nombreCorto}' requiere permisos de administrador.\n\nSe abrirá el diálogo de UAC de Windows.\n¿Continuar?", "Permisos elevados requeridos", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (confirm != MessageBoxResult.Yes) return; + var script = $"Start-Service '{nombreCorto}'"; + var bytes = System.Text.Encoding.Unicode.GetBytes(script); + var encoded = Convert.ToBase64String(bytes); + var psi = new ProcessStartInfo { FileName = "powershell.exe", - Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"Start-Service '{nombreCorto}'\"", + Arguments = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}", UseShellExecute = true, Verb = "runas", CreateNoWindow = false @@ -137,7 +142,7 @@ private async Task IniciarServicio(object? param) } catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223) { - StatusText = $"⏹️ Inicio de '{nombreCorto}' cancelado por el usuario."; + StatusText = $"⏹️ Iniciar de '{nombreCorto}' cancelado por el usuario."; } catch (Exception ex) { @@ -158,6 +163,12 @@ private void AbrirAccion(object? param) if (param is not string target) return; try { + var whitelist = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "cleanmgr", "taskmgr", "shutdown", "windowsdefender:", "firewall.cpl", + "devmgmt.msc", "eventvwr.msc", "diskmgmt.msc", "powercfg.cpl" + }; + if (target.StartsWith("expand:")) { ToggleExpandir(target["expand:".Length..]); @@ -165,7 +176,11 @@ private void AbrirAccion(object? param) } if (target.StartsWith("ms-settings:")) { - Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true }); + var allowedSettings = new[] { "ms-settings:network-troubleshoot", "ms-settings:network-status" }; + if (allowedSettings.Contains(target, StringComparer.OrdinalIgnoreCase)) + { + Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true }); + } return; } if (target == "shutdown") @@ -182,17 +197,14 @@ private void AbrirAccion(object? param) }); return; } - if (target == "taskmgr") + + if (whitelist.Contains(target)) { - Process.Start(new ProcessStartInfo { FileName = "taskmgr.exe", UseShellExecute = true }); - return; - } - if (target == "windowsdefender:") - { - Process.Start(new ProcessStartInfo { FileName = "windowsdefender:", UseShellExecute = true }); + Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true }); return; } - Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true }); + + Trace.WriteLine($"[MainViewModel] Bloqueado intento de abrir acción no segura: {target}"); } catch (Exception ex) { @@ -211,16 +223,9 @@ public string StatusText public int Puntuacion { get => _puntuacion; - set { _puntuacion = value; OnPropertyChanged(); OnPropertyChanged(nameof(ScoreColor)); OnPropertyChanged(nameof(ScoreLabel)); } + set { _puntuacion = value; OnPropertyChanged(); OnPropertyChanged(nameof(ScoreLabel)); } } - public string ScoreColor => Puntuacion switch - { - >= 80 => "#27ae60", - >= 50 => "#f39c12", - _ => "#e74c3c" - }; - public string ScoreLabel => Puntuacion switch { >= 80 => "Excelente", @@ -259,8 +264,8 @@ public bool Reparando public bool PuedeReparar => !_reparando; - public List Modulos { get; set; } = []; - public List Problemas { get; set; } = []; + public ObservableCollection Modulos { get; } = []; + public ObservableCollection Problemas { get; } = []; public List Repairs => RepairCatalog.All.ToList(); public bool HayProblemas => Problemas.Count > 0; public bool TieneDatos => Puntuacion > 0; @@ -327,18 +332,9 @@ public string ModelStatus public bool ModelListo => ModeloDescargado; private static bool ModeloDescargado => ModelDownloader.ModelExists; - private void DownloadModel() + private void RefreshModelStatus() { - RefreshModelStatus(); - try - { - Process.Start(new ProcessStartInfo - { - FileName = "https://github.com/ggerganov/llama.cpp/releases/latest", - UseShellExecute = true - }); - } - catch { } + ModelStatus = ModelDownloader.GetStatus(); } private async Task DescargarModeloAsync() @@ -372,11 +368,6 @@ private async Task DescargarModeloAsync() } } - private void RefreshModelStatus() - { - ModelStatus = ModelDownloader.GetStatus(); - } - public async Task ScanAsync() { Escaneando = true; @@ -419,12 +410,23 @@ public async Task ScanAsync() _diagnostico = _diagnostico with { Problemas = problemas, Puntuacion = puntuacion }; Puntuacion = puntuacion; - Problemas = problemas; - Modulos = BuildModulos(_diagnostico); + + Problemas.Clear(); + foreach (var p in problemas) + Problemas.Add(p); + + Modulos.Clear(); + foreach (var m in BuildModulos(_diagnostico)) + Modulos.Add(m); ScanProgress = 100; StatusText = $"Diagnóstico completado. Puntuación: {Puntuacion}/100 ({ScoreLabel}). {Problemas.Count} problema(s)."; } + catch (Exception ex) + { + StatusText = $"❌ Error al escanear: {ex.Message}"; + Trace.WriteLine($"[MainViewModel] Scan error: {ex.Message}"); + } finally { timer.Stop(); @@ -636,7 +638,7 @@ private static bool ServiceExecutableExists(ServicioInfo svc) return File.Exists(path); } - public List ChatMessages { get; set; } = []; + public ObservableCollection ChatMessages { get; } = []; public string ChatInput { get => _chatInput; diff --git a/src/SupportAI.Collectors.Windows/IColector.cs b/src/SupportAI.Collectors.Windows/IColector.cs deleted file mode 100644 index 2caa2ac..0000000 --- a/src/SupportAI.Collectors.Windows/IColector.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SupportAI.Collectors.Windows; - -public interface IColector -{ - string Name { get; } - ColectorVelocidad Speed { get; } - Task CollectAsync(CancellationToken ct = default); -} - -public enum ColectorVelocidad { Rapido, Medio, Lento } diff --git a/src/SupportAI.Collectors.Windows/PowerShellEngine.cs b/src/SupportAI.Collectors.Windows/PowerShellEngine.cs index 50c0dd5..0821092 100644 --- a/src/SupportAI.Collectors.Windows/PowerShellEngine.cs +++ b/src/SupportAI.Collectors.Windows/PowerShellEngine.cs @@ -8,19 +8,31 @@ public class PowerShellEngine { private static readonly JsonSerializerOptions JsonOpts = new() { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = null + PropertyNameCaseInsensitive = true }; public async Task CollectAllAsync(CancellationToken ct) { var script = BuildFullScript(); - var json = await RunPowerShellAsync(script, ct); - if (string.IsNullOrWhiteSpace(json)) - return new Diagnostico { GeneradoEn = DateTime.UtcNow }; + try + { + var json = await RunPowerShellAsync(script, ct); + if (string.IsNullOrWhiteSpace(json)) + return new Diagnostico { GeneradoEn = DateTime.UtcNow }; - var diag = JsonSerializer.Deserialize(json, JsonOpts); - return MapToDiagnostico(diag); + var diag = JsonSerializer.Deserialize(json, JsonOpts); + return MapToDiagnostico(diag); + } + catch (JsonException ex) + { + Trace.WriteLine($"[PowerShellEngine] JSON Deserialization error: {ex.Message}"); + return new Diagnostico { GeneradoEn = DateTime.UtcNow }; + } + catch (Exception ex) + { + Trace.WriteLine($"[PowerShellEngine] Error collecting info: {ex.Message}"); + return new Diagnostico { GeneradoEn = DateTime.UtcNow }; + } } private static string BuildFullScript() @@ -162,12 +174,16 @@ private static string BuildFullScript() """; } + private const int TimeoutSeconds = 90; + private static async Task RunPowerShellAsync(string script, CancellationToken ct) { + var bytes = System.Text.Encoding.Unicode.GetBytes(script); + var encodedScript = Convert.ToBase64String(bytes); var psi = new ProcessStartInfo { FileName = "powershell.exe", - Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{script.Replace("\"", "\\\"")}\"", + Arguments = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encodedScript}", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -181,7 +197,7 @@ private static string BuildFullScript() var readErrorTask = process.StandardError.ReadToEndAsync(ct); var processExitTask = process.WaitForExitAsync(ct); - var timeoutTask = Task.Delay(TimeSpan.FromSeconds(90), ct); + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(TimeoutSeconds), ct); var completedTask = await Task.WhenAny(processExitTask, timeoutTask); if (completedTask == timeoutTask) @@ -190,14 +206,19 @@ private static string BuildFullScript() { process.Kill(entireProcessTree: true); } - catch + catch (Exception ex) { - // Ignorar errores al matar el proceso + Trace.WriteLine($"[PowerShellEngine] Error killing process: {ex.Message}"); } return null; } var output = await readOutputTask; + var error = await readErrorTask; + if (!string.IsNullOrWhiteSpace(error)) + { + Trace.WriteLine($"[PowerShellEngine] PowerShell Stderr: {error}"); + } return string.IsNullOrEmpty(output) ? null : output.Trim(); } @@ -243,7 +264,8 @@ private static Diagnostico MapToDiagnostico(DiagnosticoRaw? raw) } : null, SO = raw.Hardware.So is { } s ? new OsInfo { - Caption = s.Caption ?? "", Version = s.Version ?? "", Build = s.Build ?? "" + Caption = s.Caption ?? "", Version = s.Version ?? "", Build = s.Build ?? "", + Instalado = s.Instalado, UltimoArranque = s.UltimoArranque } : null, Bateria = raw.Hardware.Bateria is { } bat ? new BatteryInfo { diff --git a/src/SupportAI.Core/Models/DiagnosticContext.cs b/src/SupportAI.Core/Models/DiagnosticContext.cs deleted file mode 100644 index 3c8c2b6..0000000 --- a/src/SupportAI.Core/Models/DiagnosticContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SupportAI.Core.Models; - -public class DiagnosticContext -{ - public Diagnostico Diagnostico { get; set; } = new(); - public List Log { get; } = []; - - public void LogInfo(string message) - { - Log.Add($"[{DateTime.Now:HH:mm:ss}] {message}"); - } -} diff --git a/src/SupportAI.Core/Models/DiagnosticEngine.cs b/src/SupportAI.Core/Models/DiagnosticEngine.cs index bbc8f42..8352f77 100644 --- a/src/SupportAI.Core/Models/DiagnosticEngine.cs +++ b/src/SupportAI.Core/Models/DiagnosticEngine.cs @@ -340,11 +340,11 @@ public static (List Problemas, int Puntuacion) Analyze(Diagnostico dia }); } - var puntuacion = CalcularPuntuacion(diag, problemas); + var puntuacion = CalcularPuntuacion(problemas); return (problemas, puntuacion); } - private static int CalcularPuntuacion(Diagnostico diag, List problemas) + private static int CalcularPuntuacion(List problemas) { var puntos = 100; foreach (var p in problemas) diff --git a/src/SupportAI.Core/Models/HardwareInfo.cs b/src/SupportAI.Core/Models/HardwareInfo.cs index 538885d..bc8c2f6 100644 --- a/src/SupportAI.Core/Models/HardwareInfo.cs +++ b/src/SupportAI.Core/Models/HardwareInfo.cs @@ -26,7 +26,8 @@ public record CpuInfo public record RamInfo { [JsonPropertyName("totalBytes")] public long TotalBytes { get; init; } - [JsonPropertyName("totalGB")] public double TotalGB => Math.Round(TotalBytes / 1073741824.0, 1); + private const double BytesPerGB = 1_073_741_824.0; + [JsonPropertyName("totalGB")] public double TotalGB => Math.Round(TotalBytes / BytesPerGB, 1); } public record GpuInfo @@ -46,11 +47,12 @@ public record DiscoInfo public record DiscoLogicoInfo { + private const double BytesPerGB = 1_073_741_824.0; [JsonPropertyName("letra")] public string Letra { get; init; } = ""; [JsonPropertyName("sizeBytes")] public long SizeBytes { get; init; } [JsonPropertyName("freeBytes")] public long FreeBytes { get; init; } - [JsonPropertyName("sizeGB")] public double SizeGB => Math.Round(SizeBytes / 1073741824.0, 1); - [JsonPropertyName("freeGB")] public double FreeGB => Math.Round(FreeBytes / 1073741824.0, 1); + [JsonPropertyName("sizeGB")] public double SizeGB => Math.Round(SizeBytes / BytesPerGB, 1); + [JsonPropertyName("freeGB")] public double FreeGB => Math.Round(FreeBytes / BytesPerGB, 1); [JsonPropertyName("usoPorcentaje")] public double UsoPorcentaje => SizeBytes > 0 ? Math.Round((1.0 - (double)FreeBytes / SizeBytes) * 100, 1) : 0; } diff --git a/src/SupportAI.Ia/GeminiProvider.cs b/src/SupportAI.Ia/GeminiProvider.cs index 921f905..e22c05a 100644 --- a/src/SupportAI.Ia/GeminiProvider.cs +++ b/src/SupportAI.Ia/GeminiProvider.cs @@ -19,7 +19,7 @@ public GeminiProvider(string apiKey) public async Task ChatAsync(List<(string Role, string Text)> messages, CancellationToken ct = default) { - var url = $"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key={_apiKey}"; + var url = "https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent"; var contents = messages.Where(m => m.Role != "system").Select(m => new { @@ -40,8 +40,9 @@ public async Task ChatAsync(List<(string Role, string Text)> messages, C { Content = JsonContent.Create(body) }; + request.Headers.Add("x-goog-api-key", _apiKey); - var response = await _sharedHttp.SendAsync(request, ct); + using var response = await _sharedHttp.SendAsync(request, ct); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadFromJsonAsync(ct); @@ -75,6 +76,7 @@ public async Task AnalyzeAsync(Diagnostico diag, CancellationToken HttpResponseMessage? response = null; for (int attempt = 0; attempt < 3; attempt++) { + response?.Dispose(); var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent.Create(body) @@ -98,20 +100,23 @@ public async Task AnalyzeAsync(Diagnostico diag, CancellationToken if (response == null) throw new InvalidOperationException("No se pudo obtener respuesta de Gemini."); - response.EnsureSuccessStatusCode(); + using (response) + { + response.EnsureSuccessStatusCode(); - var json = await response.Content.ReadFromJsonAsync(ct); - var text = json?.candidates?[0]?.content?.parts?[0]?.text; - if (string.IsNullOrWhiteSpace(text)) - throw new InvalidOperationException("Respuesta vacía de Gemini"); + var json = await response.Content.ReadFromJsonAsync(ct); + var text = json?.candidates?[0]?.content?.parts?[0]?.text; + if (string.IsNullOrWhiteSpace(text)) + throw new InvalidOperationException("Respuesta vacía de Gemini"); - // Extraer JSON del texto (Gemini a veces pone markdown) - var jsonStart = text.IndexOf('{'); - var jsonEnd = text.LastIndexOf('}'); - if (jsonStart >= 0 && jsonEnd > jsonStart) - text = text[jsonStart..(jsonEnd + 1)]; + // Extraer JSON del texto (Gemini a veces pone markdown) + var jsonStart = text.IndexOf('{'); + var jsonEnd = text.LastIndexOf('}'); + if (jsonStart >= 0 && jsonEnd > jsonStart) + text = text[jsonStart..(jsonEnd + 1)]; - return OpenRouterProvider.ParseResponseStatic(text, Name); + return OpenRouterProvider.ParseResponseStatic(text, Name); + } } private static string BuildPrompt(Diagnostico diag) diff --git a/src/SupportAI.Ia/LlmService.cs b/src/SupportAI.Ia/LlmService.cs index cb43e69..26869c2 100644 --- a/src/SupportAI.Ia/LlmService.cs +++ b/src/SupportAI.Ia/LlmService.cs @@ -52,7 +52,7 @@ public async Task AnalyzeAsync(Diagnostico diag, CancellationToken catch (Exception ex) when (ex is not OutOfMemoryException) { System.Diagnostics.Trace.WriteLine($"[LlmService] Error al analizar con {provider.Name}: {ex.Message}"); - if (ct.IsCancellationRequested && (ex is OperationCanceledException || ex is TaskCanceledException)) + if (ex is OperationCanceledException oce && oce.CancellationToken == ct) throw; } } diff --git a/src/SupportAI.Ia/PrivacyFilter.cs b/src/SupportAI.Ia/PrivacyFilter.cs index 80bc599..cd482af 100644 --- a/src/SupportAI.Ia/PrivacyFilter.cs +++ b/src/SupportAI.Ia/PrivacyFilter.cs @@ -5,6 +5,8 @@ namespace SupportAI.Ia; public static class PrivacyFilter { + private static readonly Regex UsersPathRegex = new(@"c:\\users\\[^\\]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SerialRegex = new(@"[A-Za-z0-9]{8,}", RegexOptions.Compiled); public static Diagnostico Anonymize(Diagnostico diag) { var hw = diag.Hardware; @@ -75,7 +77,7 @@ private static string CleanPii(string? text) if (string.IsNullOrEmpty(text)) return ""; // 1. Rutas de perfiles de usuario: C:\Users\... -> C:\Users\[USER]... - text = Regex.Replace(text, @"(?i)c:\\users\\[^\\]+", @"C:\Users\[USER]"); + text = UsersPathRegex.Replace(text, @"C:\Users\[USER]"); // 2. Nombre del usuario actual suelto text = text.Replace(Environment.UserName, "[USER]", StringComparison.OrdinalIgnoreCase); @@ -97,6 +99,6 @@ private static string CleanPii(string? text) private static string AnonymizeSerial(string value) { - return Regex.Replace(value, @"[A-Z0-9]{8,}", "[REDACTED]"); + return SerialRegex.Replace(value, "[REDACTED]"); } } diff --git a/src/SupportAI.Ia/RulesProvider.cs b/src/SupportAI.Ia/RulesProvider.cs index 3f44c31..03bcfa9 100644 --- a/src/SupportAI.Ia/RulesProvider.cs +++ b/src/SupportAI.Ia/RulesProvider.cs @@ -72,7 +72,7 @@ private void AnalizarProblema(Problema p, Diagnostico diag, StringBuilder sb, Li sb.AppendLine($" **{p.Titulo}**"); sb.AppendLine($" {p.Detalle}"); sb.AppendLine($" Sugerencia: puedes usar la herramienta 'Liberar espacio' de Windows (cleanmgr.exe) o ejecutar la reparación 'Limpiar archivos temporales' desde la app."); - sb.AppendLine($" También revisa la carpeta Descargas y Paperas de reciclaje."); + sb.AppendLine($" También revisa la carpeta Descargas y Papelera de reciclaje."); break; case "Windows" when p.Titulo.Contains("Servicios"): diff --git a/src/SupportAI.Repairs/CommandRepair.cs b/src/SupportAI.Repairs/CommandRepair.cs index cc51be1..93a5167 100644 --- a/src/SupportAI.Repairs/CommandRepair.cs +++ b/src/SupportAI.Repairs/CommandRepair.cs @@ -11,7 +11,19 @@ public abstract class CommandRepair : IRepairAction public abstract string Comando { get; } public virtual bool RequiresElevation => false; protected virtual string FileName => "powershell.exe"; - protected virtual string Arguments => $"-NoProfile -ExecutionPolicy Bypass -Command \"{Comando.Replace("\"", "\\\"")}\""; + protected virtual string Arguments + { + get + { + if (FileName.EndsWith("powershell.exe", StringComparison.OrdinalIgnoreCase)) + { + var bytes = System.Text.Encoding.Unicode.GetBytes(Comando); + var encoded = Convert.ToBase64String(bytes); + return $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}"; + } + return Comando; + } + } public virtual async Task ExecuteAsync(bool dryRun = false, CancellationToken ct = default) { @@ -51,9 +63,12 @@ private async Task RunRedirectedAsync(CancellationToken ct) }; using var process = new Process { StartInfo = psi }; process.Start(); - var output = await process.StandardOutput.ReadToEndAsync(ct); - var error = await process.StandardError.ReadToEndAsync(ct); + var readOutputTask = process.StandardOutput.ReadToEndAsync(ct); + var readErrorTask = process.StandardError.ReadToEndAsync(ct); await process.WaitForExitAsync(ct); + + var output = await readOutputTask; + var error = await readErrorTask; return RepairResult.FromProcess(process.ExitCode, output, error); }