From 9c5b75c259bd1270b22a70b0c4b1aba933693fe9 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 6 Aug 2024 16:33:40 +0200 Subject: [PATCH] CM-38979 - Add the ability to cancel scans (#19) --- CHANGELOG.md | 3 +- .../Cli/CliWrapper.cs | 18 ++++-- .../Services/CliService.cs | 27 ++++---- .../Services/CycodeService.cs | 62 +++++++++++-------- .../Services/ICliService.cs | 13 ++-- 5 files changed, 70 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c9d86..37d55b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ ## [Unreleased] -## [1.2.0] - 2024-08-XX +## [1.2.0] - 2024-08-06 - Add Open-source Threats (SCA) support +- Add the ability to cancel scans ## [1.1.4] - 2024-07-25 diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Cli/CliWrapper.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Cli/CliWrapper.cs index e8d4936..d39b7b8 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Cli/CliWrapper.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Cli/CliWrapper.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using Cycode.VisualStudio.Extension.Shared.JsonContractResolvers; using Cycode.VisualStudio.Extension.Shared.Services; @@ -22,17 +23,20 @@ private async Task GetDefaultCliArgsAsync() { // cache if (_defaultCliArgs.Length > 0) return _defaultCliArgs; - _defaultCliArgs = new[] { + _defaultCliArgs = [ "-o", "json", "--user-agent", await UserAgent.GetUserAgentEscapedAsync() - }; + ]; _logger.Debug("Default CLI args: {0}", string.Join(" ", _defaultCliArgs)); return _defaultCliArgs; } - public async Task> ExecuteCommandAsync(string[] arguments, Func cancelledCallback = null) { + public async Task> ExecuteCommandAsync( + string[] arguments, + CancellationToken cancellationToken = default + ) { General general = await General.GetLiveInstanceAsync(); ProcessStartInfo startInfo = new() { @@ -89,12 +93,14 @@ public async Task> ExecuteCommandAsync(string[] arguments, Func< process.BeginErrorReadLine(); while (!process.HasExited) { - if (cancelledCallback != null && cancelledCallback()) { + try { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1000, cancellationToken); + } catch (Exception e) when (e is ObjectDisposedException or OperationCanceledException) { process.Kill(); + _logger.Debug("CLI Execution was canceled by user"); return new CliResult.Panic(ExitCode.Termination, "Execution was canceled"); } - - await Task.Delay(1000); } int exitCode = await tcs.Task; diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CliService.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CliService.cs index e3d3168..921bec6 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CliService.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CliService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Cycode.VisualStudio.Extension.Shared.Cli; using Cycode.VisualStudio.Extension.Shared.Cli.DTO; @@ -13,8 +14,6 @@ namespace Cycode.VisualStudio.Extension.Shared.Services; -using TaskCancelledCallback = Func; - public class CliService( ILoggerService logger, IStateService stateService, @@ -79,8 +78,8 @@ private static CliResult ProcessResult(CliResult result) { } } - public async Task HealthCheckAsync(TaskCancelledCallback cancelledCallback = null) { - CliResult result = await _cli.ExecuteCommandAsync(["version"], cancelledCallback); + public async Task HealthCheckAsync(CancellationToken cancellationToken = default) { + CliResult result = await _cli.ExecuteCommandAsync(["version"], cancellationToken); CliResult processedResult = ProcessResult(result); if (processedResult is CliResult.Success successResult) { @@ -94,9 +93,9 @@ public async Task HealthCheckAsync(TaskCancelledCallback cancelledCallback return false; } - public async Task CheckAuthAsync(TaskCancelledCallback cancelledCallback = null) { + public async Task CheckAuthAsync(CancellationToken cancellationToken = default) { CliResult result = - await _cli.ExecuteCommandAsync(["auth", "check"], cancelledCallback); + await _cli.ExecuteCommandAsync(["auth", "check"], cancellationToken); CliResult processedResult = ProcessResult(result); if (processedResult is CliResult.Success successResult) { @@ -120,8 +119,8 @@ public async Task CheckAuthAsync(TaskCancelledCallback cancelledCallback = return false; } - public async Task DoAuthAsync(TaskCancelledCallback cancelledCallback = null) { - CliResult result = await _cli.ExecuteCommandAsync(["auth"], cancelledCallback); + public async Task DoAuthAsync(CancellationToken cancellationToken = default) { + CliResult result = await _cli.ExecuteCommandAsync(["auth"], cancellationToken); CliResult processedResult = ProcessResult(result); if (processedResult is not CliResult.Success successResult) { @@ -151,24 +150,24 @@ private static string[] GetCliScanOptions(CliScanType scanType) { } private async Task> ScanPathsAsync( - List paths, CliScanType scanType, TaskCancelledCallback cancelledCallback = null + List paths, CliScanType scanType, CancellationToken cancellationToken = default ) { List isolatedPaths = paths.Select(path => $"\"{path}\"").ToList(); string scanTypeString = scanType.ToString().ToLower(); CliResult result = await _cli.ExecuteCommandAsync( new[] { "scan", "-t", scanTypeString }.Concat(GetCliScanOptions(scanType)).Concat(new[] { "path" }) .Concat(isolatedPaths).ToArray(), - cancelledCallback + cancellationToken ); return ProcessResult(result); } public async Task ScanPathsSecretsAsync( - List paths, bool onDemand = true, TaskCancelledCallback cancelledCallback = null + List paths, bool onDemand = true, CancellationToken cancellationToken = default ) { CliResult results = - await ScanPathsAsync(paths, CliScanType.Secret, cancelledCallback); + await ScanPathsAsync(paths, CliScanType.Secret, cancellationToken); if (results == null) { logger.Warn("Failed to scan Secret paths: {0}", string.Join(", ", paths)); return; @@ -185,10 +184,10 @@ public async Task ScanPathsSecretsAsync( } public async Task ScanPathsScaAsync( - List paths, bool onDemand = true, TaskCancelledCallback cancelledCallback = null + List paths, bool onDemand = true, CancellationToken cancellationToken = default ) { CliResult results = - await ScanPathsAsync(paths, CliScanType.Sca, cancelledCallback); + await ScanPathsAsync(paths, CliScanType.Sca, cancellationToken); if (results == null) { logger.Warn("Failed to scan SCA paths: {0}", string.Join(", ", paths)); return; diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CycodeService.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CycodeService.cs index 5bb63b2..33e0a52 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CycodeService.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/CycodeService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using Cycode.VisualStudio.Extension.Shared.DTO; using Cycode.VisualStudio.Extension.Shared.Helpers; #if VS16 || VS17 @@ -18,7 +19,7 @@ IToolWindowMessengerService toolWindowMessengerService #if VS16 || VS17 // We don't have VS16 constant because we support range of versions in one project private static async Task WrapWithStatusCenterAsync( - Func taskFunction, + Func taskFunction, string label, bool canBeCanceled ) { @@ -32,26 +33,30 @@ bool canBeCanceled data.CanBeCanceled = canBeCanceled; ITaskHandler handler = tsc.PreRegister(options, data); - // TODO(MarshalX): Support CancellationToken! - // Task task = taskFunction(handler.UserCancellation); - Task task = taskFunction(); + Task task = taskFunction(handler.UserCancellation); handler.RegisterTask(task); - await task; // wait for the task to complete, otherwise it will be run in the background - - data.PercentComplete = 100; - handler.Progress.Report(data); + try { + await task; // wait for the task to complete, otherwise it will be run in the background + } finally { + data.PercentComplete = 100; + handler.Progress.Report(data); + } } #else private static async Task WrapWithStatusCenterAsync( - Func taskFunction, + Func taskFunction, string label, bool canBeCanceled // For old VS version; doesn't support TaskStatusCenter; doesn't support cancellation ) { // currentStep must have a value of 1 or higher! await VS.StatusBar.ShowProgressAsync(label, currentStep: 1, numberOfSteps: 2); - await taskFunction(); - await VS.StatusBar.ShowProgressAsync(label, currentStep: 2, numberOfSteps: 2); + + try { + await taskFunction(default); + } finally { + await VS.StatusBar.ShowProgressAsync(label, currentStep: 2, numberOfSteps: 2); + } } #endif @@ -74,7 +79,7 @@ await WrapWithStatusCenterAsync( ); } - private async Task InstallCliIfNeededAndCheckAuthenticationAsyncInternalAsync() { + private async Task InstallCliIfNeededAndCheckAuthenticationAsyncInternalAsync(CancellationToken cancellationToken) { try { toolWindowMessengerService.Send(MessengerCommand.LoadLoadingControl); @@ -84,8 +89,8 @@ private async Task InstallCliIfNeededAndCheckAuthenticationAsyncInternalAsync() return; } - await cliService.HealthCheckAsync(); - await cliService.CheckAuthAsync(); + await cliService.HealthCheckAsync(cancellationToken); + await cliService.CheckAuthAsync(cancellationToken); UpdateToolWindowDependingOnState(); } catch (Exception e) { @@ -101,16 +106,16 @@ await WrapWithStatusCenterAsync( ); } - private async Task StartAuthInternalAsync() { + private async Task StartAuthInternalAsync(CancellationToken cancellationToken) { if (!_pluginState.CliAuthed) { logger.Debug("Start auth..."); - await cliService.DoAuthAsync(); + await cliService.DoAuthAsync(cancellationToken); UpdateToolWindowDependingOnState(); } else { logger.Debug("Already authenticated with Cycode CLI"); } } - + public async Task StartSecretScanForCurrentProjectAsync() { string projectRoot = SolutionHelper.GetSolutionRootDirectory(); if (projectRoot == null) { @@ -127,23 +132,26 @@ public async Task StartPathSecretScanAsync(string pathToScan, bool onDemand = fa public async Task StartPathSecretScanAsync(List pathsToScan, bool onDemand = false) { await WrapWithStatusCenterAsync( - taskFunction: () => StartPathSecretScanInternalAsync(pathsToScan, onDemand), + taskFunction: cancellationToken => + StartPathSecretScanInternalAsync(pathsToScan, onDemand, cancellationToken), label: "Cycode is scanning files for hardcoded secrets...", - canBeCanceled: false // TODO(MarshalX): Should be cancellable. Not implemented yet + canBeCanceled: true ); } - private async Task StartPathSecretScanInternalAsync(List pathsToScan, bool onDemand = false) { + private async Task StartPathSecretScanInternalAsync( + List pathsToScan, bool onDemand = false, CancellationToken cancellationToken = default + ) { if (!_pluginState.CliAuthed) { logger.Debug("Not authenticated with Cycode CLI. Aborting scan..."); return; } logger.Debug("[Secret] Start scanning paths: {0}", string.Join(", ", pathsToScan)); - await cliService.ScanPathsSecretsAsync(pathsToScan, onDemand); + await cliService.ScanPathsSecretsAsync(pathsToScan, onDemand, cancellationToken); logger.Debug("[Secret] Finish scanning paths: {0}", string.Join(", ", pathsToScan)); } - + public async Task StartScaScanForCurrentProjectAsync() { string projectRoot = SolutionHelper.GetSolutionRootDirectory(); if (projectRoot == null) { @@ -160,20 +168,22 @@ public async Task StartPathScaScanAsync(string pathToScan, bool onDemand = false public async Task StartPathScaScanAsync(List pathsToScan, bool onDemand = false) { await WrapWithStatusCenterAsync( - taskFunction: () => StartPathScaScanInternalAsync(pathsToScan, onDemand), + taskFunction: cancellationToken => StartPathScaScanInternalAsync(pathsToScan, onDemand, cancellationToken), label: "Cycode is scanning files for package vulnerabilities...", - canBeCanceled: false // TODO(MarshalX): Should be cancellable. Not implemented yet + canBeCanceled: true ); } - private async Task StartPathScaScanInternalAsync(List pathsToScan, bool onDemand = false) { + private async Task StartPathScaScanInternalAsync( + List pathsToScan, bool onDemand = false, CancellationToken cancellationToken = default + ) { if (!_pluginState.CliAuthed) { logger.Debug("Not authenticated with Cycode CLI. Aborting scan..."); return; } logger.Debug("[SCA] Start scanning paths: {0}", string.Join(", ", pathsToScan)); - await cliService.ScanPathsScaAsync(pathsToScan, onDemand); + await cliService.ScanPathsScaAsync(pathsToScan, onDemand, cancellationToken); logger.Debug("[SCA] Finish scanning paths: {0}", string.Join(", ", pathsToScan)); } } \ No newline at end of file diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/ICliService.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/ICliService.cs index 817acfd..0828eed 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/ICliService.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/ICliService.cs @@ -1,18 +1,19 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using TaskCancelledCallback = System.Func; namespace Cycode.VisualStudio.Extension.Shared.Services; public interface ICliService { - Task HealthCheckAsync(TaskCancelledCallback cancelledCallback = null); - Task CheckAuthAsync(TaskCancelledCallback cancelledCallback = null); - Task DoAuthAsync(TaskCancelledCallback cancelledCallback = null); + Task HealthCheckAsync(CancellationToken cancellationToken = default); + Task CheckAuthAsync(CancellationToken cancellationToken = default); + Task DoAuthAsync(CancellationToken cancellationToken = default); Task ScanPathsSecretsAsync( - List paths, bool onDemand = true, TaskCancelledCallback cancelledCallback = null + List paths, bool onDemand = true, CancellationToken cancellationToken = default ); + Task ScanPathsScaAsync( - List paths, bool onDemand = true, TaskCancelledCallback cancelledCallback = null + List paths, bool onDemand = true, CancellationToken cancellationToken = default ); } \ No newline at end of file