From 44fefabe669f983ee08bda4efef69c5c8c41882e Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:17:23 +0530 Subject: [PATCH 1/7] PR2: Enhanced Logging with JetBrains Parity - Add comprehensive logging across all realtime scanners - Implement INFO/WARN/ERROR/DEBUG level messages - OutputPaneWriter improvements for visibility - CxAssistOutputPane enhancements - Logging parity with JetBrains plugin patterns - Scanner-specific log messages for ASCA, Secrets, IaC, Containers, OSS Base branch: feature/devassist-integration-branch Related to: AST-109633 --- .../CxAssist/Core/CxAssistOutputPane.cs | 37 +----- .../CxAssist/Realtime/Asca/AscaService.cs | 56 ++++---- .../Base/BaseRealtimeScannerService.cs | 125 +++++++++++++----- .../Realtime/Containers/ContainersService.cs | 66 ++++----- .../CxAssist/Realtime/Iac/IacService.cs | 55 ++++---- .../CxAssist/Realtime/Oss/OssService.cs | 67 +++++----- .../Realtime/RealtimeScannerOrchestrator.cs | 26 ++-- .../Realtime/Secrets/SecretsService.cs | 58 ++++---- .../CxExtension/Utils/OutputPaneWriter.cs | 21 ++- 9 files changed, 279 insertions(+), 232 deletions(-) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistOutputPane.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistOutputPane.cs index 7e03f1c2..41177ebc 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistOutputPane.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistOutputPane.cs @@ -1,51 +1,20 @@ -using System; -using EnvDTE; -using EnvDTE80; -using Microsoft.VisualStudio.Shell; using ast_visual_studio_extension.CxExtension.Utils; namespace ast_visual_studio_extension.CxExtension.CxAssist.Core { /// - /// Writes CxAssist messages to the VS Output Window ("Checkmarx" pane). - /// Same pattern as ASCAUIManager.WriteToOutputPane — main lifecycle messages only. + /// Writes CxAssist messages to the VS Output Window (shared "Checkmarx" pane via ). /// internal static class CxAssistOutputPane { - private static OutputWindowPane _outputPane; - private static bool _initialized; - - private static void EnsureInitialized() - { - if (_initialized) return; - try - { - ThreadHelper.ThrowIfNotOnUIThread(); - var dte = Package.GetGlobalService(typeof(DTE)) as DTE2; - if (dte != null) - { - var outputWindow = dte.ToolWindows.OutputWindow; - _outputPane = OutputPaneUtils.InitializeOutputPane(outputWindow, CxConstants.EXTENSION_TITLE); - } - _initialized = true; - } - catch - { - // Output pane is best-effort; swallow initialization errors - } - } - /// - /// Writes a timestamped message to the Checkmarx Output Window pane. - /// Safe to call from UI thread only (ThreadHelper.ThrowIfNotOnUIThread inside). + /// Writes a timestamped message to the Checkmarx output pane. Thread-safe (marshals to UI when needed). /// public static void WriteToOutputPane(string message) { try { - ThreadHelper.ThrowIfNotOnUIThread(); - EnsureInitialized(); - _outputPane?.OutputString($"{DateTime.Now}: {message}\n"); + OutputPaneWriter.WriteAssistLifecycle(message ?? string.Empty); } catch { diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Asca/AscaService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Asca/AscaService.cs index 5c07ab57..563faa33 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Asca/AscaService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Asca/AscaService.cs @@ -1,10 +1,12 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; -using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Asca @@ -20,6 +22,8 @@ public class AscaService : SingletonScannerBase protected override string ScannerName => "ASCA"; + protected override ScannerType CoordinatorScannerType => ScannerType.ASCA; + private AscaService(ast_visual_studio_extension.CxCLI.CxWrapper cxWrapper) : base(cxWrapper) { } @@ -46,46 +50,34 @@ public override bool ShouldScanFile(string filePath) /// /// Invokes the ASCA realtime scan CLI command. /// Maps results to Result objects for display in the findings panel. + /// Catches and logs all errors to the output pane (aligned with JetBrains error handling). /// protected override async Task ScanAndDisplayAsync(string tempFilePath, string sourceFilePath) { - var results = await _cxWrapper.ScanAscaAsync(tempFilePath, ascaLatestVersion: false); - - // Log raw JSON response - if (results != null) + try { - var jsonResponse = JsonConvert.SerializeObject(results, Formatting.Indented); - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - {jsonResponse}"); - } + var results = await _cxWrapper.ScanAscaAsync(tempFilePath, ascaLatestVersion: false); - if (results == null) - { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: null results returned - {sourceFilePath}"); - return 0; - } + if (results?.ScanDetails == null || results.ScanDetails.Count == 0) + { + ClearDisplayForFile(sourceFilePath); + return 0; + } - if (results.ScanDetails == null || results.ScanDetails.Count == 0) - { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no scan details returned - {sourceFilePath}"); - return 0; - } - - int issueCount = results.ScanDetails.Count; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({issueCount} issues found)"); + int issueCount = results.ScanDetails.Count; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: {issueCount} issue(s) found — {Path.GetFileName(sourceFilePath)}"); - // Log individual issues like JetBrains does - for (int i = 0; i < issueCount; i++) + var mappedResults = VulnerabilityMapper.FromAsca(results.ScanDetails, sourceFilePath); + CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(sourceFilePath, CoordinatorScannerType, mappedResults); + return mappedResults.Count; + } + catch (Exception ex) { - var issue = results.ScanDetails[i]; - var severity = issue.Severity ?? "UNKNOWN"; - var title = issue.RuleName ?? "Unknown Issue"; - OutputPaneWriter.WriteLine($"Issue {i + 1}: {title} [{severity}]"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Warn($"{ScannerName} scanner: scan error on {Path.GetFileName(sourceFilePath)}: {ex.Message}", ex); + ClearDisplayForFile(sourceFilePath); + return 0; } - - var mappedResults = VulnerabilityMapper.FromAsca(results.ScanDetails, sourceFilePath); - // TODO: Integrate with findings display (after CxAssistDisplayCoordinator PR merges) - // CxAssistDisplayCoordinator.UpdateFindings(buffer, mappedResults, sourceFilePath); - return mappedResults.Count; } /// diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs index 0590f9b1..100f0ea0 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs @@ -16,7 +16,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; -using static System.Diagnostics.Stopwatch; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; namespace ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base { @@ -59,6 +59,11 @@ public abstract class BaseRealtimeScannerService : IRealtimeScannerService protected abstract string ScannerName { get; } + /// + /// Scanner type used when merging into the display coordinator so clearing one engine does not remove others. + /// + protected abstract ScannerType CoordinatorScannerType { get; } + public abstract bool ShouldScanFile(string filePath); /// @@ -86,10 +91,7 @@ protected async Task ExecuteScanWithTimeoutAsync( try { if (!ValidateFileSize(filePath)) - { - OutputPaneWriter.WriteLine($"{ScannerName} scanner: Skipping {Path.GetFileName(filePath)} - file size exceeds 100MB limit"); return null; - } using (var cts = new CancellationTokenSource(SCAN_TIMEOUT_MS)) { @@ -116,7 +118,7 @@ private bool ValidateFileSize(string filePath) var fileInfo = new FileInfo(filePath); if (fileInfo.Length > MAX_FILE_SIZE_BYTES) { - OutputPaneWriter.WriteLine($"{ScannerName}: File {filePath} exceeds max size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB ({fileInfo.Length / (1024 * 1024)}MB)"); + OutputPaneWriter.WriteWarning($"{ScannerName}: Skipping {Path.GetFileName(filePath)} — file exceeds {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB limit"); return false; } return true; @@ -152,11 +154,11 @@ public virtual async Task InitializeAsync() _isInitialized = true; await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); RegisterDteRealtimeEvents(); - OutputPaneWriter.WriteLine($"{ScannerName} scanner initialized"); + OutputPaneWriter.WriteLine($"{ScannerName} scanner: initialized for real-time scanning"); } catch (Exception ex) { - OutputPaneWriter.WriteError($"Failed to initialize {ScannerName} scanner: {ex.Message}"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to initialize - {ex.Message}"); _isInitialized = false; throw; } @@ -194,10 +196,11 @@ public virtual async Task UnregisterAsync() _isSubscribed = false; _isInitialized = false; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: disabled"); } catch (Exception ex) { - OutputPaneWriter.WriteError($"Error unregistering {ScannerName} events: {ex.Message}"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to disable - {ex.Message}"); throw; } } @@ -216,9 +219,9 @@ public virtual async Task ScanExternalFileAsync(string filePath) if (!TempFileManager.TryReadVerifiedExistingFileContent(filePath, MAX_FILE_SIZE_BYTES, out var content, out var safePath)) { if (TempFileManager.TryGetVerifiedRegularFileInfo(filePath, out var fiDiag) && fiDiag.Length > MAX_FILE_SIZE_BYTES) - OutputPaneWriter.WriteLine($"{ScannerName}: File {fiDiag.FullName} exceeds max size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB ({fiDiag.Length / (1024 * 1024)}MB)"); + OutputPaneWriter.WriteWarning($"{ScannerName} scanner: skipping {fiDiag.Name} — file exceeds {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB limit"); else - OutputPaneWriter.WriteWarning($"{ScannerName} scanner: Rejecting unsafe or missing file path: {Path.GetFileName(filePath)}"); + _logger.Debug($"{ScannerName} scanner: skipping unsafe or missing file: {Path.GetFileName(filePath)}"); return; } @@ -228,6 +231,7 @@ await RunScanCoreAsync(safePath, content, bypassContentFingerprint: false, showS } catch (Exception ex) { + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(filePath)} - {ex.Message}"); _logger.Warn($"{ScannerName} scanner: ScanExternalFileAsync failed for {Path.GetFileName(filePath)}: {ex.Message}", ex); } } @@ -270,8 +274,54 @@ private void RegisterDteRealtimeEvents() if (document != null) TrySyncLineChangeBaseline(document); - OutputPaneWriter.WriteLine($"✓ {ScannerName} scanner: text editor + document events registered."); - OutputPaneWriter.WriteLine($"✓ {ScannerName} scanner: Monitoring enabled"); + OutputPaneWriter.WriteLine($"{ScannerName} scanner: monitoring enabled"); + + ScheduleActiveDocumentOpenScanAfterSubscribe(); + } + + /// + /// does not fire for documents already open when we subscribe (e.g. package.json at startup). + /// Mirrors for the active document, with short retries when DTE text is not yet hydrated. + /// + private void ScheduleActiveDocumentOpenScanAfterSubscribe() + { + _ = ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + try + { + for (int attempt = 0; attempt < 6; attempt++) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); + var document = dte?.ActiveDocument; + if (document == null) + return; + + if (!ShouldScanFile(document.FullName)) + return; + + var textDocument = (TextDocument)document.Object("TextDocument"); + if (textDocument?.StartPoint == null || textDocument?.EndPoint == null) + return; + + var content = textDocument.StartPoint.CreateEditPoint().GetText(textDocument.EndPoint); + if (!string.IsNullOrWhiteSpace(content)) + { + _debounceScheduler?.CancelPending(document.FullName); + TrySyncLineChangeBaseline(document); + await InstantScanAsync(document); + return; + } + + await Task.Delay(75); + } + } + catch (Exception ex) + { + OutputPaneWriter.WriteError($"{ScannerName}: Active document scan after subscribe failed: {ex.Message}"); + } + }); } /// @@ -374,13 +424,13 @@ private async Task ExecuteDebouncedScanAsync(string expectedPath, CancellationTo if (document.FullName.Contains("\\node_modules\\") || document.FullName.Contains("/node_modules/")) { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: file not eligible (base filter) - {document.FullName}"); + _logger.Debug($"{ScannerName} scanner: file not eligible (base filter) - {document.FullName}"); return; } if (!ShouldScanFile(document.FullName)) { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: unsupported file - {document.FullName}"); + _logger.Debug($"{ScannerName} scanner: unsupported file - {document.FullName}"); return; } @@ -391,11 +441,7 @@ private async Task ExecuteDebouncedScanAsync(string expectedPath, CancellationTo if (string.IsNullOrWhiteSpace(content)) return; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: starting scan - {document.FullName}"); - var sw = Stopwatch.StartNew(); - var count = await RunScanCoreAsync(document.FullName, content, bypassContentFingerprint: false); - sw.Stop(); - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {document.FullName} ({count} issues, {sw.ElapsedMilliseconds}ms)"); + await RunScanCoreAsync(document.FullName, content, bypassContentFingerprint: false); } private void OnDocumentOpened(Document document) @@ -411,7 +457,7 @@ private void OnDocumentOpened(Document document) _debounceScheduler?.CancelPending(document.FullName); TrySyncLineChangeBaseline(document); - OutputPaneWriter.WriteDebug($"{ScannerName}: File opened: {document.Name}, triggering instant scan"); + _logger.Debug($"{ScannerName}: File opened: {document.Name}"); _ = ThreadHelper.JoinableTaskFactory.RunAsync(async () => await InstantScanAsync(document)); } catch (Exception ex) @@ -426,7 +472,6 @@ private void OnDocumentClosing(Document document) try { - OutputPaneWriter.WriteDebug($"{ScannerName}: File closing: {document.Name}"); _debounceScheduler?.CancelPending(document.FullName); } catch (Exception ex) @@ -449,11 +494,7 @@ private async Task InstantScanAsync(Document document) if (string.IsNullOrWhiteSpace(content)) return; - var sw = Stopwatch.StartNew(); - var count = await RunScanCoreAsync(document.FullName, content, bypassContentFingerprint: true); - sw.Stop(); - - OutputPaneWriter.WriteLine($"{ScannerName} scanner: Scan completed - {Path.GetFileName(document.FullName)} ({sw.ElapsedMilliseconds}ms, {count} issue(s) found)"); + await RunScanCoreAsync(document.FullName, content, bypassContentFingerprint: true); } catch (Exception ex) { @@ -485,6 +526,8 @@ private async Task RunScanCoreAsync( string tempFilePath = null; try { + _logger.Debug($"{ScannerName} scanner: starting scan - {sourceFilePath}"); + var originalFileName = Path.GetFileName(sourceFilePath); tempFilePath = CreateTempFilePath(originalFileName, content, sourceFilePath); @@ -522,7 +565,8 @@ private async Task RunScanCoreAsync( } catch (Exception ex) { - _logger.Error($"{ScannerName} scanner: Scan error - {ex.Message}", ex); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Error($"{ScannerName} scanner: scan error - {ex.Message}", ex); return 0; } finally @@ -567,18 +611,35 @@ protected void LogScanResults( Func describeItem) { if (rawResult != null) - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - " + OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON - " + JsonConvert.SerializeObject(rawResult, Formatting.Indented)); if (items == null || items.Count == 0) { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no results returned - {sourceFilePath}"); + OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no results - {Path.GetFileName(sourceFilePath)}"); return; } - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({items.Count} {itemLabel} found)"); for (int i = 0; i < items.Count; i++) - OutputPaneWriter.WriteLine($"{itemLabel} {i + 1}: {describeItem(items[i])}"); + OutputPaneWriter.WriteDebug($"{ScannerName} {itemLabel} {i + 1}: {describeItem(items[i])}"); + } + + /// + /// Clears markers and stored findings for this scanner only on the given file; other engines' findings stay. + /// Call when a scan returns 0 results so stale markers for this engine are removed after a fix. + /// + protected void ClearDisplayForFile(string filePath) + { + if (string.IsNullOrEmpty(filePath)) return; + + try + { + Core.CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(filePath, CoordinatorScannerType, new List()); + } + catch (Exception ex) + { + OutputPaneWriter.WriteWarning($"ClearDisplayForFile failed for {filePath}: {ex.Message}"); + } } protected void LogRealtimeDetectionTelemetry(int issueCount) @@ -632,7 +693,7 @@ public virtual async Task InstantScanAsync(string filePath) if (!TempFileManager.TryReadVerifiedExistingFileContent(filePath, MAX_FILE_SIZE_BYTES, out var content, out var safePath)) { if (TempFileManager.TryGetVerifiedRegularFileInfo(filePath, out var fiDiag) && fiDiag.Length > MAX_FILE_SIZE_BYTES) - OutputPaneWriter.WriteLine($"{ScannerName}: File {fiDiag.FullName} exceeds max size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB ({fiDiag.Length / (1024 * 1024)}MB)"); + OutputPaneWriter.WriteWarning($"{ScannerName}: Skipping {fiDiag.Name} — file exceeds {MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB limit"); return; } diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Containers/ContainersService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Containers/ContainersService.cs index e40b0378..68a59327 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Containers/ContainersService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Containers/ContainersService.cs @@ -1,8 +1,9 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -22,6 +23,8 @@ public class ContainersService : SingletonScannerBase protected override string ScannerName => "Containers"; + protected override ScannerType CoordinatorScannerType => ScannerType.Containers; + private ContainersService(ast_visual_studio_extension.CxCLI.CxWrapper cxWrapper, string containersTool = "docker") : base(cxWrapper) { _containersTool = containersTool ?? "docker"; @@ -70,28 +73,30 @@ private static bool IsHelmChartPath(string fullPath) /// /// Invokes the Containers realtime scan CLI command. /// Maps results to Result objects for display in the findings panel. - /// Silently skips if Docker/Podman is not available. + /// Shows error if Docker/Podman is not available (aligned with JetBrains error handling). + /// Catches and logs all errors to the output pane. /// protected override async Task ScanAndDisplayAsync(string tempFilePath, string sourceFilePath) { - // Check if Docker/Podman is available first - bool engineExists = await _cxWrapper.CheckEngineExistAsync(_containersTool); - if (!engineExists) - { - // Silently skip if container tool is not available - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no container engine available - {sourceFilePath}"); - return 0; - } - - // CLI: cx scan containers-realtime has no --engine; _containersTool is only used for CheckEngineExistAsync above. - var results = await _cxWrapper.ContainersRealtimeScanAsync(tempFilePath, ignoredFilePath: null); - - // Log raw JSON response - if (results != null) + try { - var jsonResponse = JsonConvert.SerializeObject(results, Formatting.Indented); - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - {jsonResponse}"); - } + // Check if Docker/Podman is available first + bool engineExists = await _cxWrapper.CheckEngineExistAsync(_containersTool); + if (!engineExists) + { + OutputPaneWriter.WriteError($"{ScannerName} scanner: {_containersTool} is not available. Please ensure Docker or Podman is installed and running."); + _logger.Warn($"{ScannerName} scanner: {_containersTool} engine not found on system"); + return 0; + } + + if (new System.IO.FileInfo(tempFilePath).Length == 0) + { + OutputPaneWriter.WriteWarning($"{ScannerName} scanner: no content found in file - {Path.GetFileName(sourceFilePath)}"); + return 0; + } + + // CLI: cx scan containers-realtime has no --engine; _containersTool is only used for CheckEngineExistAsync above. + var results = await _cxWrapper.ContainersRealtimeScanAsync(tempFilePath, ignoredFilePath: null); if (results == null) { @@ -105,21 +110,20 @@ protected override async Task ScanAndDisplayAsync(string tempFilePath, stri return 0; } - int imageCount = results.Images.Count; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({imageCount} issues found)"); + int imageCount = results.Images.Count; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: {imageCount} image(s) with vulnerabilities — {Path.GetFileName(sourceFilePath)}"); - // Log individual images like JetBrains does - for (int i = 0; i < imageCount; i++) + var mappedResults = VulnerabilityMapper.FromContainers(results.Images, sourceFilePath); + CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(sourceFilePath, CoordinatorScannerType, mappedResults); + return mappedResults.Count; + } + catch (Exception ex) { - var image = results.Images[i]; - int vulnCount = image.Vulnerabilities?.Count ?? 0; - OutputPaneWriter.WriteLine($"Image {i + 1}: {image.ImageName ?? "Unknown"} - {vulnCount} vulnerabilities"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Warn($"{ScannerName} scanner: scan error on {Path.GetFileName(sourceFilePath)}: {ex.Message}", ex); + ClearDisplayForFile(sourceFilePath); + return 0; } - - var mappedResults = VulnerabilityMapper.FromContainers(results.Images, sourceFilePath); - // TODO: Integrate with findings display (after CxAssistDisplayCoordinator PR merges) - // CxAssistDisplayCoordinator.UpdateFindings(buffer, mappedResults, sourceFilePath); - return mappedResults.Count; } /// diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Iac/IacService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Iac/IacService.cs index ff50dc22..6c2c9c9e 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Iac/IacService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Iac/IacService.cs @@ -1,8 +1,9 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -20,6 +21,8 @@ public class IacService : SingletonScannerBase protected override string ScannerName => "IaC"; + protected override ScannerType CoordinatorScannerType => ScannerType.IaC; + private IacService(ast_visual_studio_extension.CxCLI.CxWrapper cxWrapper) : base(cxWrapper) { } @@ -57,40 +60,40 @@ protected override string CreateTempFilePath(string originalFileName, string con /// /// Invokes the IaC realtime scan CLI command. /// Maps results to Result objects for display in the findings panel. + /// Catches and logs all errors to the output pane (aligned with JetBrains error handling). /// protected override async Task ScanAndDisplayAsync(string tempFilePath, string sourceFilePath) { - var results = await _cxWrapper.IacRealtimeScanAsync(tempFilePath); - - // Log raw JSON response - if (results != null) + try { - var jsonResponse = JsonConvert.SerializeObject(results, Formatting.Indented); - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - {jsonResponse}"); - } + if (new System.IO.FileInfo(tempFilePath).Length == 0) + { + OutputPaneWriter.WriteWarning($"{ScannerName} scanner: no content found in file - {Path.GetFileName(sourceFilePath)}"); + return 0; + } - if (results?.Results == null || results.Results.Count == 0) - { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no results returned - {sourceFilePath}"); - return 0; - } + var results = await _cxWrapper.IacRealtimeScanAsync(tempFilePath); + + if (results?.Results == null || results.Results.Count == 0) + { + ClearDisplayForFile(sourceFilePath); + return 0; + } - int issueCount = results.Results.Count; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({issueCount} issues found)"); + int issueCount = results.Results.Count; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: {issueCount} issue(s) found — {Path.GetFileName(sourceFilePath)}"); - // Log individual issues like JetBrains does - for (int i = 0; i < issueCount; i++) + var mappedResults = VulnerabilityMapper.FromIac(results.Results, sourceFilePath); + CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(sourceFilePath, CoordinatorScannerType, mappedResults); + return mappedResults.Count; + } + catch (Exception ex) { - var issue = results.Results[i]; - var severity = issue.Severity ?? "UNKNOWN"; - var title = issue.Title ?? "Unknown Issue"; - OutputPaneWriter.WriteLine($"Issue {i + 1}: {title} [{severity}]"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Warn($"{ScannerName} scanner: scan error on {Path.GetFileName(sourceFilePath)}: {ex.Message}", ex); + ClearDisplayForFile(sourceFilePath); + return 0; } - - var mappedResults = VulnerabilityMapper.FromIac(results.Results, sourceFilePath); - // TODO: Integrate with findings display (after CxAssistDisplayCoordinator PR merges) - // CxAssistDisplayCoordinator.UpdateFindings(buffer, mappedResults, sourceFilePath); - return mappedResults.Count; } /// diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Oss/OssService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Oss/OssService.cs index c6c6aaf7..8be307a3 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Oss/OssService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Oss/OssService.cs @@ -1,8 +1,9 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -25,6 +26,8 @@ public class OssService : SingletonScannerBase protected override string ScannerName => "OSS"; + protected override ScannerType CoordinatorScannerType => ScannerType.OSS; + private OssService(ast_visual_studio_extension.CxCLI.CxWrapper cxWrapper) : base(cxWrapper) { } @@ -41,14 +44,13 @@ public override async Task InitializeAsync() string solutionRoot = RealtimeSolutionScanner.TryGetSolutionDirectory(); if (string.IsNullOrEmpty(solutionRoot)) { - OutputPaneWriter.WriteDebug("OSS scanner: manifest folder sweep skipped — no solution directory (save the solution or open a .sln)"); + OutputPaneWriter.WriteLine("OSS scanner: manifest sweep skipped — no solution directory. Save the solution or open a .sln file."); return; } if (!OssManifestSweepPolicy.ShouldScheduleFullManifestSweep(solutionRoot)) { - OutputPaneWriter.WriteDebug( - $"OSS scanner: manifest folder sweep skipped — already completed for this solution in this session ({solutionRoot})"); + _logger.Debug($"OSS scanner: manifest sweep skipped — already completed for this solution in this session ({solutionRoot})"); return; } @@ -63,7 +65,10 @@ public override async Task InitializeAsync() { await ScanAllManifestsInSolutionAsync(solutionRoot, sweepCts.Token).ConfigureAwait(false); if (!sweepCts.Token.IsCancellationRequested) + { OssManifestSweepPolicy.MarkSweepCompleted(solutionRoot); + OutputPaneWriter.WriteLine("OSS scanner: startup manifest sweep completed"); + } } catch (OperationCanceledException) { @@ -139,43 +144,43 @@ protected override string CreateTempFilePath(string originalFileName, string con /// Invokes the OSS realtime scan CLI command. /// Maps results to Result objects for display in the findings panel. /// Copies companion lock files (package-lock.json, yarn.lock) alongside the temp file. + /// Catches and logs all errors to the output pane (aligned with JetBrains error handling). /// protected override async Task ScanAndDisplayAsync(string tempFilePath, string sourceFilePath) { - // Copy companion lock file (package-lock.json / yarn.lock) alongside temp file - CopyCompanionLockFile(sourceFilePath, Path.GetDirectoryName(tempFilePath)); + try + { + if (new System.IO.FileInfo(tempFilePath).Length == 0) + { + OutputPaneWriter.WriteWarning($"{ScannerName} scanner: no content found in file - {Path.GetFileName(sourceFilePath)}"); + return 0; + } - var results = await _cxWrapper.OssRealtimeScanAsync(tempFilePath); + // Copy companion lock file (package-lock.json / yarn.lock) alongside temp file + CopyCompanionLockFile(sourceFilePath, Path.GetDirectoryName(tempFilePath)); - // Log raw JSON response - if (results != null) - { - var jsonResponse = JsonConvert.SerializeObject(results, Formatting.Indented); - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - {jsonResponse}"); - } + var results = await _cxWrapper.OssRealtimeScanAsync(tempFilePath); - if (results?.Packages == null || results.Packages.Count == 0) - { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no results returned - {sourceFilePath}"); - return 0; - } + if (results?.Packages == null || results.Packages.Count == 0) + { + ClearDisplayForFile(sourceFilePath); + return 0; + } - int packageCount = results.Packages.Count; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({packageCount} issues found)"); + int packageCount = results.Packages.Count; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: {packageCount} vulnerable package(s) found — {Path.GetFileName(sourceFilePath)}"); - // Log individual packages like JetBrains does - for (int i = 0; i < packageCount; i++) + var mappedResults = VulnerabilityMapper.FromOss(results.Packages, sourceFilePath); + CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(sourceFilePath, CoordinatorScannerType, mappedResults); + return mappedResults.Count; + } + catch (Exception ex) { - var package = results.Packages[i]; - var name = package.PackageName ?? "Unknown"; - var version = package.PackageVersion ?? "Unknown"; - OutputPaneWriter.WriteLine($"Package {i + 1}: {name}@{version}"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Warn($"{ScannerName} scanner: scan error on {Path.GetFileName(sourceFilePath)}: {ex.Message}", ex); + ClearDisplayForFile(sourceFilePath); + return 0; } - - var mappedResults = VulnerabilityMapper.FromOss(results.Packages, sourceFilePath); - // TODO: Integrate with findings display (after CxAssistDisplayCoordinator PR merges) - // CxAssistDisplayCoordinator.UpdateFindings(buffer, mappedResults, sourceFilePath); - return mappedResults.Count; } /// diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/RealtimeScannerOrchestrator.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/RealtimeScannerOrchestrator.cs index f780d83a..b8995947 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/RealtimeScannerOrchestrator.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/RealtimeScannerOrchestrator.cs @@ -4,6 +4,7 @@ using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; using ast_visual_studio_extension.CxPreferences; +using log4net; using Microsoft.VisualStudio.Shell; using System; using System.Collections.Generic; @@ -20,6 +21,7 @@ namespace ast_visual_studio_extension.CxExtension.CxAssist.Realtime /// public class RealtimeScannerOrchestrator { + private static readonly ILog _logger = LogManager.GetLogger(typeof(RealtimeScannerOrchestrator)); private readonly List _scanners = new List(); private ManifestFileWatcher _manifestWatcher; private ast_visual_studio_extension.CxCLI.CxWrapper _cxWrapper; @@ -55,6 +57,12 @@ public async Task InitializeAsync(ast_visual_studio_extension.CxCLI.CxWrapper cx _scanners.Add(scanner); } + if (_scanners.Count > 0) + { + var names = string.Join(", ", _scanners.Select(s => GetScannerName(s))); + OutputPaneWriter.WriteLine($"Realtime scanners started: {names}"); + } + var solutionRoot = GetSolutionDirectory(); // Start manifest file watcher to detect dependency/config changes @@ -98,7 +106,6 @@ private void StartManifestFileWatcher(string solutionRoot) _manifestWatcher = new ManifestFileWatcher(solutionRoot); _manifestWatcher.ManifestFileChanged += OnManifestFileChanged; _manifestWatcher.Start(); - OutputPaneWriter.WriteLine("RealtimeScannerOrchestrator: Manifest file watcher started"); } catch (Exception ex) { @@ -138,7 +145,7 @@ private void OnManifestFileChanged(string filePath, System.IO.WatcherChangeTypes try { string fileName = Path.GetFileName(filePath); - OutputPaneWriter.WriteLine($"RealtimeScannerOrchestrator: Manifest file changed: {fileName} ({changeType})"); + _logger.Debug($"Manifest file changed: {fileName} ({changeType})"); // JetBrains parity: dependency manifest rescans are OSS-only; other engines follow the active document. foreach (var scanner in _scanners) @@ -152,7 +159,7 @@ private void OnManifestFileChanged(string filePath, System.IO.WatcherChangeTypes } catch (Exception ex) { - OutputPaneWriter.WriteDebug($"RealtimeScannerOrchestrator: OSS manifest rescan for {fileName}: {ex.Message}"); + OutputPaneWriter.WriteWarning($"OSS scanner: manifest rescan failed for {fileName} - {ex.Message}"); } } } @@ -170,25 +177,25 @@ private bool ShouldInitializeRealtimeScanners(CxOneAssistSettingsModule settings { if (settings == null) { - OutputPaneWriter.WriteDebug("RealtimeScannerOrchestrator: No Assist settings module, skipping scanner initialization"); + OutputPaneWriter.WriteLine("Realtime scanners: skipping initialization — settings not available"); return false; } if (!CxPreferencesUI.IsAuthenticated()) { - OutputPaneWriter.WriteDebug("RealtimeScannerOrchestrator: User not authenticated, skipping scanner initialization"); + OutputPaneWriter.WriteLine("Realtime scanners: skipping initialization — user not authenticated"); return false; } if (!settings.McpEnabled) { - OutputPaneWriter.WriteDebug("RealtimeScannerOrchestrator: MCP disabled for tenant, skipping realtime scanners"); + OutputPaneWriter.WriteLine("Realtime scanners: skipping initialization — Checkmarx One Assist is not enabled for this tenant"); return false; } if (!settings.DevAssistLicenseEnabled && !settings.OneAssistLicenseEnabled) { - OutputPaneWriter.WriteDebug("RealtimeScannerOrchestrator: No Assist license entitlement, skipping realtime scanners"); + OutputPaneWriter.WriteLine("Realtime scanners: skipping initialization — no Checkmarx One Assist license entitlement"); return false; } @@ -210,6 +217,10 @@ public async Task UnregisterAllAsync() { await scanner.UnregisterAsync(); } + + if (_scanners.Count > 0) + OutputPaneWriter.WriteLine("Realtime scanners stopped"); + _scanners.Clear(); // Stop manifest file watcher @@ -253,7 +264,6 @@ private void StopManifestFileWatcher() _manifestWatcher.Stop(); _manifestWatcher.Dispose(); _manifestWatcher = null; - OutputPaneWriter.WriteLine("RealtimeScannerOrchestrator: Manifest file watcher stopped"); } } catch (Exception ex) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Secrets/SecretsService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Secrets/SecretsService.cs index 92fae059..83ff3c72 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Secrets/SecretsService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Secrets/SecretsService.cs @@ -1,8 +1,9 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Base; using ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils; using ast_visual_studio_extension.CxExtension.Utils; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -20,6 +21,8 @@ public class SecretsService : SingletonScannerBase protected override string ScannerName => "Secrets"; + protected override ScannerType CoordinatorScannerType => ScannerType.Secrets; + private SecretsService(ast_visual_studio_extension.CxCLI.CxWrapper cxWrapper) : base(cxWrapper) { } @@ -57,56 +60,41 @@ protected override string CreateTempFilePath(string originalFileName, string con /// Invokes the Secrets realtime scan CLI command. /// Maps results to Result objects for display in the findings panel. /// Validates that file content is not empty before scanning. + /// Catches and logs all errors to the output pane (aligned with JetBrains error handling). /// protected override async Task ScanAndDisplayAsync(string tempFilePath, string sourceFilePath) { - // Validate file is not empty (prevent scanning blank files) try { + // Validate file is not empty (prevent scanning blank files) var fileContent = System.IO.File.ReadAllText(tempFilePath); if (string.IsNullOrWhiteSpace(fileContent)) { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no content found - {sourceFilePath}"); return 0; } - } - catch (Exception ex) - { - OutputPaneWriter.WriteError($"{ScannerName} scanner: scan error - {ex.Message}"); - return 0; - } - var results = await _cxWrapper.SecretsRealtimeScanAsync(tempFilePath); + var results = await _cxWrapper.SecretsRealtimeScanAsync(tempFilePath); - // Log raw JSON response - if (results != null) - { - var jsonResponse = JsonConvert.SerializeObject(results, Formatting.Indented); - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: raw JSON response - {jsonResponse}"); - } - - if (results?.Secrets == null || results.Secrets.Count == 0) - { - OutputPaneWriter.WriteDebug($"{ScannerName} scanner: no results returned - {sourceFilePath}"); - return 0; - } + if (results?.Secrets == null || results.Secrets.Count == 0) + { + ClearDisplayForFile(sourceFilePath); + return 0; + } - int secretCount = results.Secrets.Count; - OutputPaneWriter.WriteLine($"{ScannerName} scanner: scan completed - {sourceFilePath} ({secretCount} secrets found)"); + int secretCount = results.Secrets.Count; + OutputPaneWriter.WriteLine($"{ScannerName} scanner: {secretCount} secret(s) found — {Path.GetFileName(sourceFilePath)}"); - // Log individual secrets like JetBrains does - for (int i = 0; i < secretCount; i++) + var mappedResults = VulnerabilityMapper.FromSecrets(results.Secrets, sourceFilePath); + CxAssistDisplayCoordinator.MergeUpdateFindingsForScanner(sourceFilePath, CoordinatorScannerType, mappedResults); + return mappedResults.Count; + } + catch (Exception ex) { - var secret = results.Secrets[i]; - var severity = secret.Severity ?? "UNKNOWN"; - var title = secret.Title ?? "Unknown Secret"; - OutputPaneWriter.WriteLine($"Secret {i + 1}: {title} [{severity}]"); + OutputPaneWriter.WriteError($"{ScannerName} scanner: failed to scan {Path.GetFileName(sourceFilePath)} - {ex.Message}"); + _logger.Warn($"{ScannerName} scanner: scan error on {Path.GetFileName(sourceFilePath)}: {ex.Message}", ex); + ClearDisplayForFile(sourceFilePath); + return 0; } - - var mappedResults = VulnerabilityMapper.FromSecrets(results.Secrets, sourceFilePath); - // TODO: Integrate with findings display (after CxAssistDisplayCoordinator PR merges) - // CxAssistDisplayCoordinator.UpdateFindings(buffer, mappedResults, sourceFilePath); - return mappedResults.Count; } /// diff --git a/ast-visual-studio-extension/CxExtension/Utils/OutputPaneWriter.cs b/ast-visual-studio-extension/CxExtension/Utils/OutputPaneWriter.cs index e850de74..f76b2581 100644 --- a/ast-visual-studio-extension/CxExtension/Utils/OutputPaneWriter.cs +++ b/ast-visual-studio-extension/CxExtension/Utils/OutputPaneWriter.cs @@ -20,7 +20,6 @@ public static class OutputPaneWriter { private static OutputWindowPane _checkmarxPane; private static readonly object _lockObject = new object(); - private const string PANE_NAME = "Checkmarx"; private const string PANE_PREFIX = "[Checkmarx]"; /// @@ -49,11 +48,19 @@ public static void WriteWarning(string message) } /// - /// Writes a debug message to the Checkmarx output pane with DEBUG prefix. + /// Writes a debug message only when a debugger is attached or a DEBUG build. + /// Silent in production release builds with no debugger. /// public static void WriteDebug(string message) { +#if DEBUG WriteToPane($"{PANE_PREFIX} [DEBUG] {message}"); +#else + if (System.Diagnostics.Debugger.IsAttached) + WriteToPane($"{PANE_PREFIX} [DEBUG] {message}"); + else + Debug.WriteLine($"{PANE_PREFIX} [DEBUG] {message}"); +#endif } /// @@ -65,6 +72,14 @@ public static void WriteTrace(string message) WriteToPane($"{PANE_PREFIX} [{timestamp}] {message}"); } + /// + /// Assist lifecycle messages (same pane and lock as other output; avoids a second pane from CxAssistOutputPane). + /// + public static void WriteAssistLifecycle(string message) + { + WriteToPane($"{PANE_PREFIX} {DateTime.Now}: {message}"); + } + /// /// Core method that writes to the output pane. /// Handles initialization and thread safety. @@ -121,7 +136,7 @@ private static void WriteDirectlyToPane(string message) _checkmarxPane = OutputPaneUtils.InitializeOutputPane( dte.ToolWindows.OutputWindow, - PANE_NAME); + CxConstants.OutputWindowPaneName); if (_checkmarxPane == null) { From fb13050f56e86d84ba1c3f4f1bab2ff5133524ff Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:44:02 +0530 Subject: [PATCH 2/7] Fix CS0579 Duplicate TargetFrameworkAttribute compilation error Disable auto-generation of TargetFrameworkAttribute in test project. This prevents duplicate attribute conflicts during build on some SDK configurations. Co-Authored-By: Claude Haiku 4.5 --- .../ast-visual-studio-extension-tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj index f8ca3fdc..24e5de21 100644 --- a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj +++ b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj @@ -6,6 +6,7 @@ true $(NoWarn);NU1701 + false From e4766a7d96d8f84e222880ed7329ca9cc1061cfe Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:49:07 +0530 Subject: [PATCH 3/7] Fix compilation errors: add missing OutputWindowPaneName, MergeUpdateFindingsForScanner, and downgrade Newtonsoft.Json - Add OutputWindowPaneName constant to CxConstants for output pane initialization - Implement MergeUpdateFindingsForScanner method in CxAssistDisplayCoordinator to merge scanner-specific findings - Downgrade Newtonsoft.Json from 13.0.3 to 13.0.1 to match pipeline constraints - All scanner services (ASCA, Secrets, IaC, Containers, OSS) now have required method Co-Authored-By: Claude Haiku 4.5 --- .../ast-visual-studio-extension-tests.csproj | 2 +- .../Core/CxAssistDisplayCoordinator.cs | 48 +++++++++++++++++++ .../CxExtension/Utils/CxConstants.cs | 3 ++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj index 24e5de21..35d40644 100644 --- a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj +++ b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs index 467aee6d..06b84d43 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs @@ -233,6 +233,54 @@ public static List FindAllVulnerabilitiesForLine(Vulnerability v) } } + /// + /// Merges findings from a specific scanner type into existing findings for a file. + /// Replaces all findings of that scanner type for the file with the new list. + /// Updates gutter, underlines, and the findings window in one call. + /// + /// File path to update findings for. + /// Scanner type to merge findings for. + /// New findings for this scanner; if empty/null, clears findings of this scanner type for the file. + public static void MergeUpdateFindingsForScanner(string filePath, ScannerType scannerType, List newFindings) + { + if (string.IsNullOrEmpty(filePath)) return; + + string key = NormalizePath(filePath); + if (string.IsNullOrEmpty(key)) return; + + IReadOnlyDictionary> snapshot; + lock (_lock) + { + // Get existing findings for this file + List merged = new List(); + if (_fileToIssues.TryGetValue(key, out var existing) && existing != null) + { + // Keep all findings NOT from this scanner + merged.AddRange(existing.FindAll(v => v.Scanner != scannerType)); + } + + // Add the new findings from this scanner + if (newFindings != null && newFindings.Count > 0) + { + merged.AddRange(newFindings.FindAll(v => CxAssistConstants.IsScannerEnabled(v.Scanner))); + } + + // Update the map + if (merged.Count == 0) + _fileToIssues.Remove(key); + else + _fileToIssues[key] = merged; + + // Snapshot for IssuesUpdated event + var copy = new Dictionary>(_fileToIssues.Count, StringComparer.OrdinalIgnoreCase); + foreach (var kv in _fileToIssues) + copy[kv.Key] = new List(kv.Value); + snapshot = copy; + } + + IssuesUpdated?.Invoke(snapshot); + } + /// /// Updates gutter icons, underlines (squiggles), and stored findings for the problem window in one call. /// Stores issues per file and raises IssuesUpdated so the findings window can stay in sync (reference-like). diff --git a/ast-visual-studio-extension/CxExtension/Utils/CxConstants.cs b/ast-visual-studio-extension/CxExtension/Utils/CxConstants.cs index 2888afb9..1c80927c 100644 --- a/ast-visual-studio-extension/CxExtension/Utils/CxConstants.cs +++ b/ast-visual-studio-extension/CxExtension/Utils/CxConstants.cs @@ -118,5 +118,8 @@ internal class CxConstants public static string AUTH_VALIDATE_ERROR => "Error in authentication"; public static string AUTH_LOGOUT_SUCCESS => "You have successfully logged out"; + /************ OUTPUT WINDOW ************/ + public static string OutputWindowPaneName => "Checkmarx"; + } } From 5c91627fbe7c88e4858604b7425f194beb94b042 Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:54:47 +0530 Subject: [PATCH 4/7] Upgraded version --- .../ast-visual-studio-extension-tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj index 35d40644..24e5de21 100644 --- a/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj +++ b/ast-visual-studio-extension-tests/ast-visual-studio-extension-tests.csproj @@ -14,7 +14,7 @@ - + From 1bd147d07e057d4cd86b1d5ae60527acbdfe7020 Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:03:07 +0530 Subject: [PATCH 5/7] Replace VulnerabilityMapper with complete implementation from feature/AST-109633-ui-manager-changes Synced VulnerabilityMapper with the production-ready implementation that includes: - Proper MapSeverity() method for all scanner severity levels - JetBrains-aligned ASCA column resolution with file reading - Secrets grouping by secret type (one Vulnerability per Secret) - IaC per-location Vulnerability creation - Container scanning with per-CVE and per-image handling - OSS/SCA package scanning with per-CVE and per-package handling - Proper 0-based to 1-based line number conversion - VulnerabilityLocation handling for multi-line ranges - GetHighestSeverity() for display purposes All FromXxx() methods now return List instead of Result, fixing the type mismatch errors in all 5 scanner services. Co-Authored-By: Claude Haiku 4.5 --- .../Realtime/Utils/VulnerabilityMapper.cs | 546 ++++++++++++------ 1 file changed, 382 insertions(+), 164 deletions(-) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs index c8f769e2..1e7383e8 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs @@ -1,137 +1,176 @@ using ast_visual_studio_extension.CxCLI; +using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.Enums; using ast_visual_studio_extension.CxWrapper.Models; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using CoreSeverity = ast_visual_studio_extension.CxExtension.CxAssist.Core.Models.SeverityLevel; namespace ast_visual_studio_extension.CxExtension.CxAssist.Realtime.Utils { /// /// Converts realtime scan results from various scanners (ASCA, Secrets, IaC, Containers, OSS) - /// into the unified Result model used throughout the VS extension. + /// into the unified Vulnerability model used throughout the VS extension. /// /// This mapper standardizes severity levels, generates unique IDs, and populates common fields - /// to ensure consistent UI display across all scanner types. + /// to ensure consistent UI display across all scanner types. Aligned with JetBrains adaptor pattern: + /// raw CLI results → domain model directly (no Result wrapper). /// public static class VulnerabilityMapper { + /// + /// Maps raw severity string to typed CoreSeverity enum. + /// Handles all scanner-specific severity labels and CLI variations. + /// + private static CoreSeverity MapSeverity(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + return CoreSeverity.Medium; + + switch (raw.Trim().ToLowerInvariant()) + { + case "malicious": + return CoreSeverity.Malicious; + case "critical": + return CoreSeverity.Critical; + case "high": + case "error": + return CoreSeverity.High; + case "medium": + case "warning": + return CoreSeverity.Medium; + case "low": + case "info": + case "informational": + return CoreSeverity.Low; + case "ok": + return CoreSeverity.Ok; + case "ignored": + return CoreSeverity.Ignored; + default: + return CoreSeverity.Unknown; + } + } /// - /// Maps ASCA scan details to Result objects. - /// Groups issues by line number and creates one Result per group. + /// Maps ASCA scan details to Vulnerability objects. + /// Groups issues by line number and creates one Vulnerability per group. /// Uses AscaResultGrouper for consistent, reusable grouping logic. + /// Aligned with JetBrains AscaScanResultAdaptor. /// - public static List FromAsca(List details, string filePath) + public static List FromAsca(List details, string filePath) { if (details == null || details.Count == 0) - return new List(); + return new List(); - var results = new List(); + var vulnerabilities = new List(); - // Use AscaResultGrouper for consistent grouping and severity sorting - var issueGroups = AscaResultGrouper.GroupByLineAndSortBySeverity(details); + // JetBrains style: one Vulnerability per issue (no grouping), sorted by line then severity + var sorted = details + .OrderBy(d => d.Line) + .ThenBy(d => MapSeverity(d.Severity)) + .ToList(); - // Create one Result per group - foreach (var group in issueGroups) + foreach (var detail in sorted) { - if (!AscaResultGrouper.IsValidGroup(group)) - continue; - - var primaryIssue = group.PrimaryIssue; - - // If multiple issues on same line, indicate count in title - var title = group.HasMultipleIssues - ? $"{group.Details.Count} multiple ASCA issues" - : primaryIssue.RuleName; + int column1Based = ResolveAscaColumn1Based(filePath, detail); + int start0 = detail.Length > 0 ? Math.Max(0, column1Based - 1) : 0; + int end0 = detail.Length > 0 ? start0 + detail.Length : 0; - var result = new Result + var vuln = new Vulnerability { - Id = UniqueIdGenerator.GenerateId(primaryIssue.Line, primaryIssue.RuleId.ToString(), primaryIssue.FileName), - Type = "ASCA", - Severity = SeverityMapper.MapToString(primaryIssue.Severity), - Status = "Urgent", - State = "Active", - Description = group.HasMultipleIssues - ? $"{group.Details.Count} issues found: {string.Join("; ", group.Details.Select(d => d.RuleName))}" - : primaryIssue.Description, - Data = new Data - { - QueryId = primaryIssue.RuleId.ToString(), - QueryName = title, - FileName = primaryIssue.FileName, - Line = primaryIssue.Line, - LanguageName = primaryIssue.Language, - Group = "ASCA", - RuleName = title, - RuleDescription = group.HasMultipleIssues - ? $"{group.Details.Count} ASCA issues on line {primaryIssue.Line}" - : primaryIssue.Description, - Remediation = primaryIssue.RemediationAdvise - } + Id = UniqueIdGenerator.GenerateId(detail.Line, detail.RuleId.ToString(), detail.FileName), + Title = detail.RuleName, + Description = detail.Description, + Severity = MapSeverity(detail.Severity), + Scanner = ScannerType.ASCA, + LineNumber = detail.Line, + EndLineNumber = detail.Line, + ColumnNumber = column1Based, + StartIndex = start0, + EndIndex = end0, + FilePath = filePath, + RuleName = detail.RuleName, + RemediationAdvice = detail.RemediationAdvise }; - results.Add(result); + vulnerabilities.Add(vuln); } - return results; + return vulnerabilities; } /// - /// Maps Secrets scan results to Result objects. - /// Each secret location becomes a separate Result. + /// Maps Secrets scan results to Vulnerability objects. + /// Groups all locations of the same secret into a SINGLE Vulnerability. + /// Problem descriptor uses only the FIRST location (matches JetBrains SecretsScanResultAdaptor). + /// All locations are preserved in the Vulnerability.Locations list for reference. /// - public static List FromSecrets(List secrets, string filePath) + public static List FromSecrets(List secrets, string filePath) { if (secrets == null || secrets.Count == 0) - return new List(); + return new List(); - var results = new List(); + var vulnerabilities = new List(); foreach (var secret in secrets) { if (secret.Locations == null || secret.Locations.Count == 0) continue; + // Use FIRST location for the main vulnerability entry (displayed in Error List and gutter) + var firstLocation = secret.Locations[0]; + var allLocations = new List(); + + // Collect ALL locations for the vulnerability (for reference/grouping) foreach (var location in secret.Locations) { - var result = new Result + allLocations.Add(new VulnerabilityLocation { - Id = UniqueIdGenerator.GenerateLocationBasedId(location.Line, location.StartIndex, location.EndIndex, filePath), - Type = "Secret Detection", - Severity = SeverityMapper.MapToString(secret.Severity), - Status = "Urgent", - State = "Active", - Description = secret.Description, - Data = new Data - { - QueryName = secret.Title, - FileName = filePath, - Line = location.Line, - Group = "Secrets", - RuleName = secret.Title, - RuleDescription = secret.Description, - Value = secret.SecretValue - } - }; - - results.Add(result); + Line = location.Line + 1, // CLI returns 0-based; convert to 1-based + StartIndex = location.StartIndex, + EndIndex = location.EndIndex + }); } + + // Create ONE Vulnerability per Secret (grouped by secret type) + var vuln = new Vulnerability + { + // Use first location for unique ID (matches JetBrains logic) + Id = UniqueIdGenerator.GenerateLocationBasedId(firstLocation.Line, firstLocation.StartIndex, firstLocation.EndIndex, filePath), + Title = secret.Title, + Description = secret.Description, + Severity = MapSeverity(secret.Severity), + Scanner = ScannerType.Secrets, + LineNumber = firstLocation.Line + 1, // Problem descriptor displays only first location + EndLineNumber = firstLocation.Line + 1, + ColumnNumber = firstLocation.StartIndex, + StartIndex = firstLocation.StartIndex, + EndIndex = firstLocation.EndIndex, + Locations = allLocations, // All locations (but only first is displayed in gutter) + FilePath = filePath, + SecretType = secret.Title + }; + + vulnerabilities.Add(vuln); } - return results; + return vulnerabilities; } /// - /// Maps IaC scan results to Result objects. - /// Each IaC issue location becomes a separate Result. + /// Maps IaC scan results to Vulnerability objects. + /// Each IaC issue location becomes a separate Vulnerability. + /// Aligned with JetBrains IacScanResultAdaptor. /// - public static List FromIac(List issues, string filePath) + public static List FromIac(List issues, string filePath) { if (issues == null || issues.Count == 0) - return new List(); + return new List(); - var results = new List(); + var vulnerabilities = new List(); foreach (var issue in issues) { @@ -140,155 +179,334 @@ public static List FromIac(List issues, string filePath) foreach (var location in issue.Locations) { - var result = new Result + var vuln = new Vulnerability { Id = UniqueIdGenerator.GenerateLocationBasedId(location.Line, location.StartIndex, location.EndIndex, filePath), - Type = "IaC", - Severity = SeverityMapper.MapToString(issue.Severity), - Status = "Urgent", - State = "Active", + Title = issue.Title, Description = issue.Description, - SimilarityId = issue.SimilarityId, - Data = new Data + Severity = MapSeverity(issue.Severity), + Scanner = ScannerType.IaC, + LineNumber = location.Line + 1, // CLI returns 0-based; convert to 1-based + EndLineNumber = location.Line + 1, + ColumnNumber = location.StartIndex, + StartIndex = location.StartIndex, + EndIndex = location.EndIndex, + Locations = new List { - QueryName = issue.Title, - FileName = filePath, - Line = location.Line, - Group = "IaC", - RuleName = issue.Title, - RuleDescription = issue.Description, - ExpectedValue = issue.ExpectedValue, - Value = issue.ActualValue - } + new VulnerabilityLocation + { + Line = location.Line + 1, + StartIndex = location.StartIndex, + EndIndex = location.EndIndex + } + }, + FilePath = filePath, + ExpectedValue = issue.ExpectedValue, + ActualValue = issue.ActualValue }; - results.Add(result); + vulnerabilities.Add(vuln); } } - return results; + return vulnerabilities; } /// - /// Maps Container scan results to Result objects. - /// Each vulnerability in each container image location becomes a separate Result. + /// Maps Container scan results to Vulnerability objects. + /// Each container image becomes one or more Vulnerabilities: one per CVE (if image has vulnerabilities), + /// or one for the image itself with severity = image Status (if no vulnerabilities). + /// Aligned with JetBrains ContainerScanResultAdaptor: one ScanIssue per image. /// - public static List FromContainers(List images, string filePath) + public static List FromContainers(List images, string filePath) { if (images == null || images.Count == 0) - return new List(); + return new List(); - var results = new List(); + var vulnerabilities = new List(); foreach (var image in images) { - if (image.Vulnerabilities == null || image.Vulnerabilities.Count == 0) - continue; - if (image.Locations == null || image.Locations.Count == 0) continue; - foreach (var vuln in image.Vulnerabilities) + // Convert all locations for this image once (reuse for all CVEs in this image) + var imageLocations = new List(); + foreach (var location in image.Locations) { - foreach (var location in image.Locations) + imageLocations.Add(new VulnerabilityLocation { - var result = new Result + Line = location.Line + 1, // CLI returns 0-based; convert to 1-based + StartIndex = location.StartIndex, + EndIndex = location.EndIndex + }); + } + + // Case 1: Image has vulnerabilities → create one Vulnerability per CVE + if (image.Vulnerabilities != null && image.Vulnerabilities.Count > 0) + { + foreach (var vuln in image.Vulnerabilities) + { + var vulnerability = new Vulnerability { - Id = UniqueIdGenerator.GenerateId(location.Line, vuln.Cve ?? image.ImageName, filePath), - Type = "Container Vulnerability", - Severity = SeverityMapper.MapToString(vuln.Severity), - Status = "Urgent", - State = "Active", + Id = UniqueIdGenerator.GenerateId( + image.Locations[0].Line, + vuln.Cve ?? image.ImageName, + filePath), + Title = $"{image.ImageName}:{image.ImageTag}", Description = $"Vulnerability {vuln.Cve} found in {image.ImageName}:{image.ImageTag}", - Data = new Data - { - QueryName = $"{image.ImageName}:{image.ImageTag}", - FileName = filePath, - Line = location.Line, - Group = "Containers", - RuleName = vuln.Cve ?? "Container Vulnerability", - RuleDescription = $"Container image {image.ImageName}:{image.ImageTag} contains vulnerability {vuln.Cve}" - } + Severity = MapSeverity(vuln.Severity), + Scanner = ScannerType.Containers, + LineNumber = image.Locations[0].Line + 1, + EndLineNumber = image.Locations[0].Line + 1, + ColumnNumber = image.Locations[0].StartIndex, + StartIndex = image.Locations[0].StartIndex, + EndIndex = image.Locations[0].EndIndex, + Locations = new List(imageLocations), + FilePath = filePath, + CveName = vuln.Cve, + PackageName = image.ImageName, + PackageVersion = image.ImageTag }; - - results.Add(result); + vulnerabilities.Add(vulnerability); } } + // Case 2: Image has no vulnerabilities → create one entry per image with Status severity + else + { + var vulnerability = new Vulnerability + { + Id = UniqueIdGenerator.GenerateId( + image.Locations[0].Line, + image.ImageName, + filePath), + Title = $"{image.ImageName}:{image.ImageTag}", + Description = $"No vulnerabilities found in {image.ImageName}:{image.ImageTag}", + // Use image Status as severity (aligned with JetBrains ContainerScanResultAdaptor line 99) + Severity = MapSeverity(image.Status ?? "ok"), + Scanner = ScannerType.Containers, + LineNumber = image.Locations[0].Line + 1, + EndLineNumber = image.Locations[0].Line + 1, + ColumnNumber = image.Locations[0].StartIndex, + StartIndex = image.Locations[0].StartIndex, + EndIndex = image.Locations[0].EndIndex, + Locations = new List(imageLocations), + FilePath = filePath, + PackageName = image.ImageName, + PackageVersion = image.ImageTag + }; + vulnerabilities.Add(vulnerability); + } } - return results; + return vulnerabilities; } /// - /// Maps OSS/SCA scan results to Result objects. - /// Each vulnerability in each package location becomes a separate Result. + /// Maps OSS/SCA scan results to Vulnerability objects. + /// Each package becomes one or more Vulnerabilities: one per CVE (if package has vulnerabilities), + /// or one for the package itself with severity = package.Status (if no vulnerabilities). + /// Aligned with JetBrains OssScanResultAdaptor: one ScanIssue per package. /// - public static List FromOss(List packages, string filePath) + public static List FromOss(List packages, string filePath) { if (packages == null || packages.Count == 0) - return new List(); + return new List(); - var results = new List(); + var vulnerabilities = new List(); foreach (var package in packages) { - if (package.Vulnerabilities == null || package.Vulnerabilities.Count == 0) - continue; - if (package.Locations == null || package.Locations.Count == 0) continue; - foreach (var vuln in package.Vulnerabilities) + // Convert all locations for this package once (reuse for all CVEs in this package) + var packageLocations = new List(); + foreach (var location in package.Locations) { - foreach (var location in package.Locations) + packageLocations.Add(new VulnerabilityLocation { - var result = new Result + Line = location.Line + 1, // CLI returns 0-based; convert to 1-based + StartIndex = location.StartIndex, + EndIndex = location.EndIndex + }); + } + + // Case 1: Package has vulnerabilities → create one Vulnerability per CVE + if (package.Vulnerabilities != null && package.Vulnerabilities.Count > 0) + { + foreach (var vuln in package.Vulnerabilities) + { + var vulnerability = new Vulnerability { - Id = UniqueIdGenerator.GeneratePackageId(package.PackageName, package.PackageVersion, filePath), - Type = "OSS/SCA Vulnerability", - Severity = SeverityMapper.MapToString(vuln.Severity), - Status = "Urgent", - State = "Active", + // Unique ID per CVE within the package + Id = UniqueIdGenerator.GenerateId( + package.Locations[0].Line, + vuln.Cve ?? package.PackageName, + filePath), + Title = $"{package.PackageName}@{package.PackageVersion}", Description = vuln.Description, - Data = new Data - { - QueryName = $"{package.PackageName}@{package.PackageVersion}", - FileName = filePath, - Line = location.Line, - Group = "OSS", - PackageIdentifier = $"{package.PackageManager}:{package.PackageName}", - RuleName = vuln.Cve ?? "OSS Vulnerability", - RuleDescription = vuln.Description, - Value = vuln.FixVersion - } + Severity = MapSeverity(vuln.Severity), + Scanner = ScannerType.OSS, + LineNumber = package.Locations[0].Line + 1, + EndLineNumber = package.Locations[0].Line + 1, + ColumnNumber = package.Locations[0].StartIndex, + StartIndex = package.Locations[0].StartIndex, + EndIndex = package.Locations[0].EndIndex, + Locations = new List(packageLocations), + FilePath = filePath, + PackageName = package.PackageName, + PackageVersion = package.PackageVersion, + PackageManager = package.PackageManager, + RecommendedVersion = vuln.FixVersion, + CveName = vuln.Cve }; - - results.Add(result); + vulnerabilities.Add(vulnerability); } } + // Case 2: Package has no vulnerabilities → create one entry per package with Status severity + // (e.g., "ok" packages with no CVEs still get a finding entry, severity = Ok) + else + { + var vulnerability = new Vulnerability + { + Id = UniqueIdGenerator.GeneratePackageId(package.PackageName, package.PackageVersion, filePath), + Title = $"{package.PackageName}@{package.PackageVersion}", + Description = $"No vulnerabilities found in {package.PackageName}@{package.PackageVersion}", + // Use package Status (e.g., "ok") as severity — aligned with JetBrains line 99 + Severity = MapSeverity(package.Status ?? "ok"), + Scanner = ScannerType.OSS, + LineNumber = package.Locations[0].Line + 1, + EndLineNumber = package.Locations[0].Line + 1, + ColumnNumber = package.Locations[0].StartIndex, + StartIndex = package.Locations[0].StartIndex, + EndIndex = package.Locations[0].EndIndex, + Locations = new List(packageLocations), + FilePath = filePath, + PackageName = package.PackageName, + PackageVersion = package.PackageVersion, + PackageManager = package.PackageManager + }; + vulnerabilities.Add(vulnerability); + } } - return results; + return vulnerabilities; } /// - /// Gets the highest severity from a list of Results. - /// Used for display purposes (e.g., showing worst-case severity in UI). + /// ASCA column for Findings / Error List. + /// JetBrains (ast-jetbrains-plugin): AscaScanResultAdaptor does not set Location start/end; ProblemBuilder.build + /// uses DevAssistUtils.getTextRangeForLine — the problem range is the trimmed full line, so the Problems column is the + /// 1-based index of the first non-whitespace character on that line (not problematicLine substring position). + /// Order here: CLI column/start_column if present, else trimmed-line start (JetBrains parity), else problematicLine match, else 1. /// - public static SeverityLevel GetHighestSeverity(List results) + private static int ResolveAscaColumn1Based(string filePath, CxAscaDetail detail) { - if (results == null || results.Count == 0) - return SeverityLevel.Medium; + if (detail == null) + return 1; + if (detail.Column > 0) + return detail.Column; - var severities = results - .Where(r => !string.IsNullOrEmpty(r.Severity)) - .Select(r => SeverityMapper.MapToLevel(r.Severity)) - .ToList(); + int jetBrainsStyleColumn = TryFirstNonWhitespaceColumn1Based(filePath, detail.Line); + if (jetBrainsStyleColumn > 0) + return jetBrainsStyleColumn; + + int fromSnippet = TryComputeAscaColumnFromProblematicLine(filePath, detail.Line, detail.ProblematicLine); + if (fromSnippet > 0) + return fromSnippet; + + return 1; + } + + /// + /// Matches DevAssistUtils.getTextRangeForLine leading-whitespace trim: first non-whitespace column, 1-based. + /// + private static int TryFirstNonWhitespaceColumn1Based(string filePath, int line1Based) + { + string actualLine = TryReadSourceLine(filePath, line1Based); + if (string.IsNullOrEmpty(actualLine)) + return 0; + + for (int i = 0; i < actualLine.Length; i++) + { + if (!char.IsWhiteSpace(actualLine[i])) + return i + 1; + } + + return 0; + } + + private static int TryComputeAscaColumnFromProblematicLine(string filePath, int line1Based, string problematicLine) + { + if (line1Based < 1 || string.IsNullOrEmpty(filePath)) + return 0; - if (severities.Count == 0) - return SeverityLevel.Medium; + string actualLine = TryReadSourceLine(filePath, line1Based); + if (string.IsNullOrEmpty(actualLine) || string.IsNullOrWhiteSpace(problematicLine)) + return 0; - return severities.OrderBy(s => SeverityMapper.GetPrecedence(s.ToString())).First(); + string probe = problematicLine.TrimEnd('\r', '\n'); + if (probe.Length == 0) + return 0; + + int idx = actualLine.IndexOf(probe, StringComparison.Ordinal); + if (idx >= 0) + return idx + 1; + + string trimmedProbe = probe.Trim(); + if (trimmedProbe.Length > 0) + { + idx = actualLine.IndexOf(trimmedProbe, StringComparison.Ordinal); + if (idx >= 0) + return idx + 1; + + string trimmedActual = actualLine.TrimStart(); + int leadingWs = actualLine.Length - trimmedActual.Length; + idx = trimmedActual.IndexOf(trimmedProbe, StringComparison.Ordinal); + if (idx >= 0) + return leadingWs + idx + 1; + } + + return 0; + } + + private static string TryReadSourceLine(string filePath, int line1Based) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + return null; + try + { + int n = 0; + foreach (var line in File.ReadLines(filePath)) + { + n++; + if (n == line1Based) + return line; + } + } + catch + { + // File locked, transient IO, etc. + } + + return null; + } + + /// + /// Gets the highest severity from a list of Vulnerabilities. + /// Used for display purposes (e.g., showing worst-case severity in UI). + /// + public static CoreSeverity GetHighestSeverity(List vulnerabilities) + { + if (vulnerabilities == null || vulnerabilities.Count == 0) + return CoreSeverity.Medium; + + // Order by ordinal value of enum (lower = higher precedence based on enum definition) + return vulnerabilities + .Select(v => v.Severity) + .OrderBy(s => (int)s) + .First(); } } } From 2ea459c2bdff21700ebc7cb13f07bc42cfd9c363 Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:07:04 +0530 Subject: [PATCH 6/7] Fix ASCA Column resolution - remove reference to non-existent Column property CxAscaDetail does not have a Column property. Removed the check for it and rely on first non-whitespace column detection or problematicLine matching. Co-Authored-By: Claude Haiku 4.5 --- .../CxAssist/Realtime/Utils/VulnerabilityMapper.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs index 1e7383e8..f62ce811 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs @@ -396,17 +396,12 @@ public static List FromOss(List packages, /// /// ASCA column for Findings / Error List. - /// JetBrains (ast-jetbrains-plugin): AscaScanResultAdaptor does not set Location start/end; ProblemBuilder.build - /// uses DevAssistUtils.getTextRangeForLine — the problem range is the trimmed full line, so the Problems column is the - /// 1-based index of the first non-whitespace character on that line (not problematicLine substring position). - /// Order here: CLI column/start_column if present, else trimmed-line start (JetBrains parity), else problematicLine match, else 1. + /// Uses first non-whitespace column (JetBrains parity), or falls back to problematicLine match, or 1. /// private static int ResolveAscaColumn1Based(string filePath, CxAscaDetail detail) { if (detail == null) return 1; - if (detail.Column > 0) - return detail.Column; int jetBrainsStyleColumn = TryFirstNonWhitespaceColumn1Based(filePath, detail.Line); if (jetBrainsStyleColumn > 0) From 4f14e64e47502e7b93d3d56422039e0e75ead09a Mon Sep 17 00:00:00 2001 From: Rahul Pidde <206018639+cx-rahul-pidde@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:40:07 +0530 Subject: [PATCH 7/7] Fix code quality issues in CxAssistDisplayCoordinator and BaseRealtimeScannerService CxAssistDisplayCoordinator: - Add enabled-scanner filter in MergeUpdateFindingsForScanner() for consistency with UpdateFindings() and UpdateFindingsForFile() - Remove "for testing" reference from UpdateFindingsForFile() production API documentation - Clarify NormalizePath() defensive fallback behavior in comments BaseRealtimeScannerService: - Protect 5 TextDocument casting locations from InvalidCastException using safe 'as' operator with try-catch - Added in: OnDocumentOpened, TrySyncLineChangeBaseline, OnTextChanged, OnDocumentClosing, InstantScanAsync - Ensures scanner doesn't crash on unexpected COM object types Co-Authored-By: Claude Haiku 4.5 --- .../Core/CxAssistDisplayCoordinator.cs | 7 +-- .../Base/BaseRealtimeScannerService.cs | 53 +++++++++++++++++-- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs index 06b84d43..c4b2185f 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs @@ -57,6 +57,7 @@ private static void OnThemeChanged() /// /// Normalizes a file path for use as the per-file map key (same file always maps to the same key). + /// Returns the original path if normalization fails, ensuring the key is never empty for valid inputs. /// private static string NormalizePath(string path) { @@ -68,7 +69,7 @@ private static string NormalizePath(string path) catch (Exception ex) { CxAssistErrorHandler.LogAndSwallow(ex, "DisplayCoordinator.NormalizePath"); - return path; + return path ?? string.Empty; } } @@ -259,10 +260,10 @@ public static void MergeUpdateFindingsForScanner(string filePath, ScannerType sc merged.AddRange(existing.FindAll(v => v.Scanner != scannerType)); } - // Add the new findings from this scanner + // Add the new findings from this scanner (only if scanner is enabled) if (newFindings != null && newFindings.Count > 0) { - merged.AddRange(newFindings.FindAll(v => CxAssistConstants.IsScannerEnabled(v.Scanner))); + merged.AddRange(newFindings.FindAll(v => v != null && CxAssistConstants.IsScannerEnabled(v.Scanner))); } // Update the map diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs index 100f0ea0..740013ef 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Base/BaseRealtimeScannerService.cs @@ -301,7 +301,16 @@ private void ScheduleActiveDocumentOpenScanAfterSubscribe() if (!ShouldScanFile(document.FullName)) return; - var textDocument = (TextDocument)document.Object("TextDocument"); + TextDocument textDocument; + try + { + textDocument = document.Object("TextDocument") as TextDocument; + } + catch (InvalidCastException ex) + { + _logger.Debug($"{ScannerName}: Failed to get TextDocument: {ex.Message}"); + return; + } if (textDocument?.StartPoint == null || textDocument?.EndPoint == null) return; @@ -358,7 +367,15 @@ private void TrySyncLineChangeBaseline(Document document) { try { - var textDocument = (TextDocument)document.Object("TextDocument"); + TextDocument textDocument; + try + { + textDocument = document.Object("TextDocument") as TextDocument; + } + catch (InvalidCastException) + { + return; + } if (textDocument?.StartPoint == null || textDocument?.EndPoint == null) return; _lineChangeBaselinePath = document.FullName; @@ -380,7 +397,15 @@ private void OnTextChanged(TextPoint startPoint, TextPoint endPoint, int hint) try { - var textDocument = (TextDocument)document.Object("TextDocument"); + TextDocument textDocument; + try + { + textDocument = document.Object("TextDocument") as TextDocument; + } + catch (InvalidCastException) + { + return; + } if (textDocument == null) return; var currentContent = textDocument.StartPoint.CreateEditPoint().GetText(textDocument.EndPoint); @@ -434,7 +459,16 @@ private async Task ExecuteDebouncedScanAsync(string expectedPath, CancellationTo return; } - var textDocument = (TextDocument)document.Object("TextDocument"); + TextDocument textDocument; + try + { + textDocument = document.Object("TextDocument") as TextDocument; + } + catch (InvalidCastException ex) + { + _logger.Debug($"{ScannerName}: Failed to get TextDocument: {ex.Message}"); + return; + } if (textDocument?.StartPoint == null || textDocument?.EndPoint == null) return; var content = textDocument.StartPoint.CreateEditPoint().GetText(textDocument.EndPoint); @@ -486,7 +520,16 @@ private async Task InstantScanAsync(Document document) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var textDocument = (TextDocument)document.Object("TextDocument"); + TextDocument textDocument; + try + { + textDocument = document.Object("TextDocument") as TextDocument; + } + catch (InvalidCastException ex) + { + _logger.Debug($"{ScannerName}: Failed to get TextDocument: {ex.Message}"); + return; + } if (textDocument?.StartPoint == null || textDocument?.EndPoint == null) return;