diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2f1cd..1d00ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## [Unreleased] +- Add tree view filtering by severity - Add proper support for disabled modules - Fix scrolling issue in violation panels diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs new file mode 100644 index 0000000..33f01ee --- /dev/null +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs @@ -0,0 +1,88 @@ +using Cycode.VisualStudio.Extension.Shared.DTO; +using Cycode.VisualStudio.Extension.Shared.Services; + +namespace Cycode.VisualStudio.Extension.Shared.Commands; + +internal static class TreeViewFilterBySeverityCommand { + public static void Execute(OleMenuCommand command, string severity) { + ITemporaryStateService tempState = + ServiceLocator.GetService(); + IToolWindowMessengerService toolWindowMessengerService = + ServiceLocator.GetService(); + + /* + * We must flip the bool first to reflect the new state. + * + * Unchecked means that we do not want to see that severity in the tree view. + * In other words, if the button highlighted, it means that we WANT to see that severity in the tree view. + */ + command.Checked = !command.Checked; + bool isFilterEnabled = !command.Checked; + + switch (severity.ToLower()) { + case "critical": + tempState.IsTreeViewFilterByCriticalSeverityEnabled = isFilterEnabled; + break; + case "high": + tempState.IsTreeViewFilterByHighSeverityEnabled = isFilterEnabled; + break; + case "medium": + tempState.IsTreeViewFilterByMediumSeverityEnabled = isFilterEnabled; + break; + case "low": + tempState.IsTreeViewFilterByLowSeverityEnabled = isFilterEnabled; + break; + case "info": + tempState.IsTreeViewFilterByInfoSeverityEnabled = isFilterEnabled; + break; + } + + // refresh tree view to apply filter + toolWindowMessengerService.Send(new MessageEventArgs(MessengerCommand.TreeViewRefresh)); + } +} + +[Command(PackageIds.TreeViewFilterByCriticalSeverityCommand)] +internal sealed class TreeViewFilterByCriticalSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "critical"); + } + } +} + +[Command(PackageIds.TreeViewFilterByHighSeverityCommand)] +internal sealed class TreeViewFilterByHighSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "high"); + } + } +} + +[Command(PackageIds.TreeViewFilterByMediumSeverityCommand)] +internal sealed class TreeViewFilterByMediumSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "medium"); + } + } +} + +[Command(PackageIds.TreeViewFilterByLowSeverityCommand)] +internal sealed class TreeViewFilterByLowSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "low"); + } + } +} + +[Command(PackageIds.TreeViewFilterByInfoSeverityCommand)] +internal sealed class TreeViewFilterByInfoSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "info"); + } + } +} diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs index 6fcefe2..5304b0f 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs @@ -22,6 +22,9 @@ public partial class CycodeTreeViewControl { private static readonly IToolWindowMessengerService _toolWindowMessengerService = ServiceLocator.GetService(); + + private static readonly ITemporaryStateService _tempState = + ServiceLocator.GetService(); private static readonly ILoggerService _logger = ServiceLocator.GetService(); @@ -90,6 +93,28 @@ private static int GetSeverityWeight(string severity) { _ => 0 }; } + + private static HashSet GetEnabledSeverityFilters() { + HashSet enabledSeverityFilters = []; + + if (_tempState.IsTreeViewFilterByCriticalSeverityEnabled) { + enabledSeverityFilters.Add("critical"); + } + if (_tempState.IsTreeViewFilterByHighSeverityEnabled) { + enabledSeverityFilters.Add("high"); + } + if (_tempState.IsTreeViewFilterByMediumSeverityEnabled) { + enabledSeverityFilters.Add("medium"); + } + if (_tempState.IsTreeViewFilterByLowSeverityEnabled) { + enabledSeverityFilters.Add("low"); + } + if (_tempState.IsTreeViewFilterByInfoSeverityEnabled) { + enabledSeverityFilters.Add("info"); + } + + return enabledSeverityFilters; + } private static string GetRootNodeSummary(IEnumerable sortedDetections) { // detections must be sorted by severity already @@ -115,7 +140,11 @@ private void CreateDetectionNodes( IEnumerable detections, Func createNodeCallback ) { - List sortedDetections = detections + HashSet enabledSeverityFilters = GetEnabledSeverityFilters(); + List severityFilteredDetections = detections + .Where(detection => !enabledSeverityFilters.Contains(detection.Severity.ToLower())) + .ToList(); + List sortedDetections = severityFilteredDetections .OrderByDescending(detection => GetSeverityWeight(detection.Severity)) .ToList(); IEnumerable> detectionsByFile = diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems b/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems index 1438902..d0bd3f9 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems @@ -49,6 +49,7 @@ + diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs index e0b5329..129ae41 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs @@ -2,9 +2,9 @@ global using Community.VisualStudio.Toolkit; global using Microsoft.VisualStudio.Shell; global using Task = System.Threading.Tasks.Task; +using System.ComponentModel.Design; using System.Runtime.InteropServices; using System.Threading; -using Community.VisualStudio.Toolkit; using Cycode.VisualStudio.Extension.Shared.Components.ToolWindows; using Cycode.VisualStudio.Extension.Shared.Options; using Cycode.VisualStudio.Extension.Shared.Sentry; @@ -12,7 +12,6 @@ using Cycode.VisualStudio.Extension.Shared.Services.ErrorList; using Cycode.VisualStudio.Extension.Shared.Services.ErrorTagger; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.Shell; namespace Cycode.VisualStudio.Extension.Shared; @@ -64,6 +63,23 @@ protected override async Task InitializeAsync( cycodeService.InstallCliIfNeededAndCheckAuthenticationAsync().FireAndForget(); logger.Info("CycodePackage.InitializeAsync completed."); + + // set the filter commands to checked because they are enabled by default (no actual filtering) + if (await GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService commandService) { + int[] filterCommands = [ + PackageIds.TreeViewFilterByCriticalSeverityCommand, + PackageIds.TreeViewFilterByHighSeverityCommand, + PackageIds.TreeViewFilterByMediumSeverityCommand, + PackageIds.TreeViewFilterByLowSeverityCommand, + PackageIds.TreeViewFilterByInfoSeverityCommand + ]; + foreach (int commandId in filterCommands) { + MenuCommand cmd = commandService.FindCommand(new CommandID(PackageGuids.Cycode, commandId)); + if (cmd != null) { + cmd.Checked = true; + } + } + } } private static void OnSettingsSaved(General obj) { diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs index ef75c9d..5f6ce70 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs @@ -13,6 +13,12 @@ public interface ITemporaryStateService { bool IsIacScanningEnabled { get; } bool IsSastScanningEnabled { get; } bool IsAiLargeLanguageModelEnabled { get; } + + bool IsTreeViewFilterByCriticalSeverityEnabled { get; set; } + bool IsTreeViewFilterByHighSeverityEnabled { get; set; } + bool IsTreeViewFilterByMediumSeverityEnabled { get; set; } + bool IsTreeViewFilterByLowSeverityEnabled { get; set; } + bool IsTreeViewFilterByInfoSeverityEnabled { get; set; } } public class TemporaryStateService : ITemporaryStateService { @@ -37,6 +43,12 @@ public StatusResult CliStatus { public bool IsSastScanningEnabled => _cliStatus?.SupportedModules?.SastScanning == true; public bool IsAiLargeLanguageModelEnabled => _cliStatus?.SupportedModules?.AiLargeLanguageModel == true; + public bool IsTreeViewFilterByCriticalSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByHighSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByMediumSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByLowSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByInfoSeverityEnabled { get; set; } = false; + public TemporaryStateService(ILoggerService loggerService) { _loggerService = loggerService; _loggerService.Info("CycodeTemporaryStateService init"); diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs index dd1c8ea..226ba79 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs @@ -14,7 +14,8 @@ internal sealed class PackageGuids { internal sealed class PackageIds { public const int ViewOpenToolWindowCommand = 0x1001; - public const int TWindowToolbar = 0x1050; + public const int TWindowToolbar = 0x1030; + public const int ToolbarOpenSettingsCommand = 0x1052; public const int ToolbarRunAllScansCommand = 0x1053; public const int ToolbarOpenWebDocsCommand = 0x1054; @@ -23,6 +24,12 @@ internal sealed class PackageIds { public const int TreeViewExpandAllCommand = 0x1057; public const int TreeViewCollapseAllCommand = 0x1058; + public const int TreeViewFilterByCriticalSeverityCommand = 0x1059; + public const int TreeViewFilterByHighSeverityCommand = 0x1060; + public const int TreeViewFilterByMediumSeverityCommand = 0x1061; + public const int TreeViewFilterByLowSeverityCommand = 0x1062; + public const int TreeViewFilterByInfoSeverityCommand = 0x1063; + public const int TopMenuCycodeCommand = 0x1103; public const int TopMenuOpenSettingsCommand = 0x1104; } \ No newline at end of file diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct index 86d59bd..2fb7c0a 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct @@ -33,42 +33,77 @@ + + + + +