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
diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs
index 467aee6d..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;
}
}
@@ -233,6 +234,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 (only if scanner is enabled)
+ if (newFindings != null && newFindings.Count > 0)
+ {
+ merged.AddRange(newFindings.FindAll(v => v != null && 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/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..740013ef 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,63 @@ 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;
+
+ 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);
+ 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}");
+ }
+ });
}
///
@@ -308,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;
@@ -330,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);
@@ -374,28 +449,33 @@ 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;
}
- 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);
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 +491,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 +506,6 @@ private void OnDocumentClosing(Document document)
try
{
- OutputPaneWriter.WriteDebug($"{ScannerName}: File closing: {document.Name}");
_debounceScheduler?.CancelPending(document.FullName);
}
catch (Exception ex)
@@ -441,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;
@@ -449,11 +537,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 +569,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 +608,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 +654,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 +736,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/CxAssist/Realtime/Utils/VulnerabilityMapper.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Realtime/Utils/VulnerabilityMapper.cs
index c8f769e2..f62ce811 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,329 @@ 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.
+ /// Uses first non-whitespace column (JetBrains parity), or falls back to problematicLine match, or 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;
- 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();
}
}
}
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";
+
}
}
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)
{