for LocationListener interception -
+ * Informational (enableClickableActions=false): renders plain text with Ctrl+1
+ * hint
+ *
+ * For ASCA/IAC issues that group multiple vulnerabilities on the same line,
+ * renders one block per vulnerability instead of collapsing to root attributes.
+ */
+public final class CheckmarxProblemDescriptionFormatter {
+
+ private static final Map DESCRIPTION_ICON = new LinkedHashMap<>();
+
+ private static final String COUNT = "COUNT";
+ private static final String PACKAGE = "Package";
+ private static final String DEV_ASSIST = "DevAssist";
+ private static final String CONTAINER = "Container";
+
+ public CheckmarxProblemDescriptionFormatter() {
+ initIconsMap();
+ }
+
+ private static void initIconsMap() {
+ DESCRIPTION_ICON.put(SeverityLevel.MALICIOUS.getSeverity(),
+ getImage(DevAssistConstants.ImagePaths.MALICIOUS_PNG));
+ DESCRIPTION_ICON.put(SeverityLevel.CRITICAL.getSeverity(),
+ getImage(DevAssistConstants.ImagePaths.CRITICAL_PNG));
+ DESCRIPTION_ICON.put(SeverityLevel.HIGH.getSeverity(), getImage(DevAssistConstants.ImagePaths.HIGH_PNG));
+ DESCRIPTION_ICON.put(SeverityLevel.MEDIUM.getSeverity(), getImage(DevAssistConstants.ImagePaths.MEDIUM_PNG));
+ DESCRIPTION_ICON.put(SeverityLevel.LOW.getSeverity(), getImage(DevAssistConstants.ImagePaths.LOW_PNG));
+
+ DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.CRITICAL.getSeverity()),
+ getImage(DevAssistConstants.ImagePaths.CRITICAL_16_PNG));
+ DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.HIGH.getSeverity()),
+ getImage(DevAssistConstants.ImagePaths.HIGH_16_PNG));
+ DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.MEDIUM.getSeverity()),
+ getImage(DevAssistConstants.ImagePaths.MEDIUM_16_PNG));
+ DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.LOW.getSeverity()),
+ getImage(DevAssistConstants.ImagePaths.LOW_16_PNG));
+
+ DESCRIPTION_ICON.put(PACKAGE, getImage(DevAssistConstants.ImagePaths.PACKAGE_PNG));
+ DESCRIPTION_ICON.put(DEV_ASSIST, getImage(DevAssistConstants.ImagePaths.DEV_ASSIST_PNG));
+ DESCRIPTION_ICON.put(CONTAINER, getImage(DevAssistConstants.ImagePaths.CONTAINER_PNG));
+ }
+
+ /**
+ * Build the HTML body (without outer html/body tags) describing the issue,
+ * suitable for embedding inside a BrowserInformationControl or merging with
+ * other annotations' hover text on the same line.
+ *
+ * Supports both clickable action links (for CheckmarxAnnotationHover's
+ * BrowserInformationControl) and informational-only links (for marker
+ * resolution fallback).
+ *
+ * @param issue the scan issue
+ * @param enableClickableActions if true, renders action links as #action:...
+ * for LocationListener interception; if false,
+ * renders as informational text with Ctrl+1 hint
+ * @param textColor text color in hex format (e.g., "#000000" for dark themes,
+ * "#FFFFFF" for light), or null to use inherited color
+ * @return HTML fragment
+ */
+ public String formatDescriptionHtml(ScanIssue scanIssue, boolean enableClickableActions, String textColor) {
+ StringBuilder descBuilder = new StringBuilder();
+
+ // DevAssist image
+ descBuilder.append(TABLE_WITH_TR).append("
")
+ .append(DESCRIPTION_ICON.get(DEV_ASSIST)).append(" | ");
+ descBuilder.append("
");
+
+ // For ASCA and IAC multiple violations
+ appendMultipleViolationsTitle(descBuilder, scanIssue, textColor);
+
+ switch (scanIssue.getScanEngine()) {
+ case OSS:
+ buildOSSDescription(descBuilder, scanIssue, textColor);
+ break;
+ case ASCA:
+ buildASCADescription(descBuilder, scanIssue, textColor);
+ break;
+ case SECRETS:
+ buildSecretsDescription(descBuilder, scanIssue, textColor);
+ break;
+ case IAC:
+ buildIACDescription(descBuilder, scanIssue, textColor);
+ break;
+ case CONTAINERS:
+ buildContainerDescription(descBuilder, scanIssue);
+ break;
+ default:
+ buildDefaultDescription(descBuilder, scanIssue);
+ }
+ if (scanIssue.getScanEngine() != ScanEngine.IAC && scanIssue.getScanEngine() != ScanEngine.ASCA) {
+ buildRemediationActionsSection(descBuilder, scanIssue.getScanIssueId(), scanIssue.getScanEngine().name());
+ }
+ return descBuilder.toString();
+ }
+
+ /**
+ * Builds the OSS description for the provided scan issue and appends it to the
+ * given StringBuilder. This method incorporates severity-specific formatting,
+ * including handling for malicious packages, and assembles the description with
+ * the package header and vulnerability details.
+ *
+ * @param descBuilder the StringBuilder to which the formatted OSS description
+ * will be appended
+ * @param scanIssue the ScanIssue object containing information about the
+ * scanned issue, including its severity, vulnerabilities,
+ * and related details
+ */
+ private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ buildPackageMessage(descBuilder, scanIssue, textColor);
+ buildVulnerabilitySection(descBuilder, scanIssue);
+ }
+
+ /**
+ * Builds the package header section of a description for a scan issue and
+ * appends it to the provided StringBuilder. This method formats information
+ * about the scan issue's severity, title, and package version, and includes an
+ * associated image icon representing the issue.
+ *
+ * @param descBuilder the StringBuilder to which the formatted package header
+ * information will be appended
+ * @param scanIssue the ScanIssue object containing details about the issue
+ * such as severity, title, and package version
+ */
+ private static void buildPackageMessage(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ String secondaryText = DevAssistConstants.SEVERITY_PACKAGE;
+ String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : "";
+ String iconKey = PACKAGE;
+ if (scanIssue.getSeverity().equalsIgnoreCase(SeverityLevel.MALICIOUS.getSeverity())) {
+ secondaryText = PACKAGE;
+ iconKey = scanIssue.getSeverity();
+ }
+ String icon = getSeverityIconHtml(iconKey, ICON_INLINE_STYLE);
+
+ descBuilder.append(TABLE_WITH_TR).append("").append(icon)
+ .append(" | ").append("").append(" ").append("")
+ .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append("@")
+ .append(HtmlEscapeUtil.escape(scanIssue.getPackageVersion())).append("").append(" - ").append(HtmlEscapeUtil.escape(scanIssue.getSeverity()))
+ .append(" ").append(HtmlEscapeUtil.escape(secondaryText)).append(" | ");
+ }
+
+ /**
+ * Builds the vulnerability section of a scan issue description and appends it
+ * to the provided StringBuilder. This method processes the list of
+ * vulnerabilities associated with the scan issue, categorizes them by severity,
+ * and includes detailed descriptions for specific vulnerabilities where
+ * applicable.
+ *
+ * @param descBuilder the StringBuilder to which the formatted vulnerability
+ * section will be appended
+ * @param scanIssue the ScanIssue object containing details about the scan,
+ * including associated vulnerabilities
+ */
+ private void buildVulnerabilitySection(StringBuilder descBuilder, ScanIssue scanIssue) {
+ List vulnerabilityList = scanIssue.getVulnerabilities();
+ if (vulnerabilityList == null || vulnerabilityList.isEmpty()) {
+ return;
+ }
+ descBuilder.append("").append(TABLE_WITH_TR);
+ Map vulnerabilityCount = getVulnerabilityCount(vulnerabilityList);
+ DESCRIPTION_ICON.forEach((severity, iconPath) -> {
+ Long count = vulnerabilityCount.get(severity);
+ if (count != null && count > 0) {
+ descBuilder.append("| ")
+ .append(DESCRIPTION_ICON.get(getSeverityCountIconKey(severity))).append(" | ")
+ .append("")
+ .append(count).append(" | ");
+ }
+ });
+ descBuilder.append("
");
+ }
+
+ /**
+ * ASCA description. Format: [Title for multiple issues] [Severity Icon] Title
+ * (bold) - description - SAST vulnerability
+ */
+ private void buildASCADescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) {
+ String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE);
+ descBuilder.append(TABLE_WITH_TR_IAC_ASCA)
+ .append("").append(severityIcon)
+ .append(" | ");
+ String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : "";
+ descBuilder.append("")
+ .append("")
+ .append(" ").append("")
+ .append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("").append(" - ")
+ .append(HtmlEscapeUtil.escape(vulnerability.getDescription())).append(" - SAST vulnerability").append(" ")
+ .append(" | ");
+ buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(), scanIssue.getScanEngine().name());
+ }
+ }
+
+
+ /**
+ * Secrets description. Format: [Severity Icon] Title (bold) - Secret finding
+ */
+ private void buildSecretsDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ String icon = getSeverityIconHtml(scanIssue.getSeverity(), ICON_INLINE_STYLE);
+ String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : "";
+ descBuilder.append(TABLE_WITH_TR).append("").append(icon)
+ .append(" | ").append("").append(" ").append("")
+ .append(HtmlEscapeUtil.escape(formatTitle(scanIssue.getTitle()))).append("")
+ .append(" - Secret finding")
+ .append(" | ");
+ }
+
+ /**
+ * IAC description (image header + vulnerability description with Title).
+ */
+ private void buildIACDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) {
+ String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE);
+ descBuilder.append(TABLE_WITH_TR_IAC_ASCA)
+ .append("").append(severityIcon)
+ .append(" | ");
+ String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : "";
+ descBuilder
+ .append("")
+ .append(" ")
+ .append("").append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("").append(" - ")
+ .append(HtmlEscapeUtil.escape(vulnerability.getActualValue())).append(" ")
+ .append(HtmlEscapeUtil.escape(vulnerability.getDescription()))
+ .append(" IaC vulnerability")
+ .append(" | ");
+ buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(),
+ scanIssue.getScanEngine().name());
+ }
+ }
+
+ /**
+ * Container description (image header + vulnerability counts).
+ */
+ private void buildContainerDescription(StringBuilder descBuilder, ScanIssue scanIssue) {
+ buildImageHeader(descBuilder, scanIssue);
+ buildVulnerabilitySection(descBuilder, scanIssue);
+ }
+
+ /**
+ * Builds the default description for a scan issue and appends it to the
+ * provided StringBuilder. This method formats basic details about the scan
+ * issue, including its title and description.
+ *
+ * @param descBuilder the StringBuilder to which the formatted default
+ * description will be appended
+ * @param scanIssue the ScanIssue object containing details about the issue
+ * such as title and description
+ */
+ /**
+ * Default fallback description.
+ */
+ private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) {
+ descBuilder.append("").append(scanIssue.getTitle()).append(" -").append(scanIssue.getDescription());
+ }
+
+ /**
+ * Container image header.
+ */
+ private void buildImageHeader(StringBuilder descBuilder, ScanIssue scanIssue) {
+ String icon = getSeverityIconHtml(CONTAINER, ICON_INLINE_STYLE);
+
+ descBuilder.append(TABLE_WITH_TR).append("
").append(icon)
+ .append(" | ").append("
").append(" ").append("")
+ .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append("@")
+ .append(HtmlEscapeUtil.escape(scanIssue.getImageTag())).append("").append(" | ");
+ }
+
+ /**
+ * Calculates the count of vulnerabilities grouped by their severity levels.
+ * This method processes a list of vulnerabilities, retrieves their severity,
+ * and returns a map where the keys are severity levels and the values are the
+ * counts.
+ *
+ * @param vulnerabilityList the list of vulnerabilities to be grouped and
+ * counted by severity
+ * @return a map where the key is the severity level and the value is the count
+ * of vulnerabilities at that severity
+ */
+ private Map
getVulnerabilityCount(List vulnerabilityList) {
+ return vulnerabilityList.stream().map(Vulnerability::getSeverity)
+ .collect(Collectors.groupingBy(severity -> severity, Collectors.counting()));
+ }
+
+ /**
+ * Legacy overload for backward compatibility: defaults to informational action
+ * links (non-clickable) and no text color override.
+ */
+ public String formatDescriptionHtml(ScanIssue issue) {
+ return formatDescriptionHtml(issue, false, null);
+ }
+
+
+ /**
+ * Builds the remediation actions section of the description.
+ *
+ * @param descBuilder {@link StringBuilder} object to add the remediation
+ * actions section to.
+ * @param scanIssueId {@link String} object containing the remediation actions
+ * section data.
+ */
+ private void buildRemediationActionsSection(StringBuilder descBuilder, String scanIssueId, String engineName) {
+ String buttonStyle = "color: #4470EC; cursor: pointer; " + TITLE_FONT_SIZE + TITLE_FONT_FAMILY
+ + CELL_LINE_HEIGHT_STYLE + "white-space: nowrap; margin:0; padding:0;";
+
+ // Add CSS for hover effect with underline - more specific selector with !important to ensure it applies
+ descBuilder.append("");
+
+ descBuilder.append(
+ "
");
+ }
+
+
+ /**
+ * Injects inline styles into an existing HTML image tag.
+ */
+ private static String getSeverityIconHtml(String key, String extraStyle) {
+ String imgTag = DESCRIPTION_ICON.getOrDefault(key, "");
+
+ if (imgTag == null || imgTag.isEmpty()) {
+ return "";
+ }
+
+ if (imgTag.contains("style='")) {
+ return imgTag.replaceFirst("style='", "style='" + extraStyle);
+ } else if (imgTag.contains("style=\"")) {
+ return imgTag.replaceFirst("style=\"", "style=\"" + extraStyle);
+ } else {
+ int insertPos = imgTag.indexOf("/>");
+
+ return insertPos > 0
+ ? imgTag.substring(0, insertPos) + " style='" + extraStyle + "'" + imgTag.substring(insertPos)
+ : imgTag;
+ }
+ }
+
+ /**
+ * Inline styles matching JetBrains' ProblemDescription.InlineStyle. Ensures
+ * visual consistency with JetBrains plugin design.
+ */
+ static class InlineStyle {
+
+ private InlineStyle() {
+ }
+
+ // Table layout: icon (20px) in first column, content in second column
+ static final String TABLE_WITH_TR = "";
+ static final String TABLE_WITH_TR_IAC_ASCA = "";
+
+ static final String TABLE_WITH_TR_FULL_WIDTH = "";
+
+ // Typography styles
+ static final String TITLE_FONT_FAMILY = "font-family: sans-serif";
+ static final String TITLE_FONT_SIZE = "font-size:12px;";
+ static final String CELL_LINE_HEIGHT_STYLE = "line-height:16px;vertical-align:middle;";
+
+ // Secondary text (severity labels like "SAST vulnerability", "IaC
+ // vulnerability")
+ static final String SECONDARY_SPAN_STYLE = "display:inline-block;vertical-align:middle;line-height:16px;font-size:11px;color:#ADADAD;"
+ + "font-family:system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;";
+
+ // Icon column style (20px wide, right-padded)
+ static final String ICON_COLUMN_STYLE = "width:20px;padding:0 6px 0 0;vertical-align:middle;";
+
+ // Content column style
+ static final String CONTENT_COLUMN_STYLE = "padding:0 4px;white-space:normal;" + TITLE_FONT_SIZE
+ + TITLE_FONT_FAMILY + CELL_LINE_HEIGHT_STYLE;
+
+ /**
+ * Default inline severity icon style used consistently across all engines.
+ */
+ static final String ICON_INLINE_STYLE = "display:inline-block;vertical-align:middle;max-height:16px;line-height:16px;";
+ }
+
+ /**
+ * Appends multiple violations title for ASCA and IAC engines when there are
+ * multiple vulnerabilities. This method adds a formatted title showing the
+ * number of violations detected.
+ *
+ * @param descBuilder the StringBuilder to append the title to
+ * @param scanIssue the ScanIssue containing information about vulnerabilities
+ */
+ private static void appendMultipleViolationsTitle(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) {
+ String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : "";
+ if (scanIssue.getVulnerabilities() == null || scanIssue.getVulnerabilities().size() <= 1) {
+ return;
+ }
+ boolean isASCAOrIAC = scanIssue.getScanEngine() == ScanEngine.ASCA
+ || scanIssue.getScanEngine() == ScanEngine.IAC;
+ if (isASCAOrIAC) {
+ descBuilder.append(TABLE_WITH_TR).append("| ").append(" ")
+ .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append(" Checkmarx One Assist")
+ .append(" |
");
+ }
+ }
+
+ /**
+ * Generates an HTML image element based on the provided icon name.
+ *
+ * @param iconPath the path to the image file that will be used in the HTML
+ * content
+ * @return a String representing an HTML image element with the provided icon
+ * path
+ */
+ private static String getImage(String iconPath) {
+ String imagePath = DevAssistUtils.themeBasedPNGIconForHtmlImage(iconPath);
+ if (imagePath == null || imagePath.isEmpty()) {
+ return "";
+ }
+ try {
+ URL imageUrl = new URL(imagePath);
+ if (imageUrl != null) {
+ URL fileUrl = FileLocator.toFileURL(imageUrl);
+ String urlString = fileUrl.toString();
+ return "
";
+ }
+ } catch (Exception e) {
+ return "";
+ }
+ return "";
+ }
+
+ /**
+ * Formats a kebab-case title into Title-Case (e.g., "generic-api-key" ->
+ * "Generic-Api-Key").
+ *
+ * @param title The kebab-case title string.
+ * @return A formatted Title-Case string.
+ */
+ private String formatTitle(String title) {
+ if (title == null || title.isEmpty()) {
+ return "";
+ }
+ return Arrays.stream(title.split("-")).map(
+ word -> word.isEmpty() ? "" : Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase())
+ .collect(Collectors.joining("-"));
+ }
+
+ /**
+ * Returns the key for the icon representing the specified severity with a count
+ * suffix.
+ *
+ * @param severity the severity
+ * @return the key for the icon representing the specified severity with a count
+ * suffix
+ */
+ private static String getSeverityCountIconKey(String severity) {
+ return severity + COUNT;
+ }
+
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java
index 1a996ca6..b5138d10 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java
@@ -6,6 +6,7 @@
import org.eclipse.ui.plugin.AbstractUIPlugin;
import com.checkmarx.eclipse.devassist.backend.Constants;
+import com.checkmarx.eclipse.devassist.utils.DevAssistUtils;
/**
* Registry for managing Checkmarx severity icons.
@@ -56,32 +57,49 @@ private static void initializeRegistry() {
imageRegistry = PlatformUI.getWorkbench().getDisplay() != null
? new ImageRegistry(PlatformUI.getWorkbench().getDisplay())
: new ImageRegistry();
-
- // Register small icons (16px)
- registerIcon("malicious_16", "icons/severity/malicious_16.svg");
- registerIcon("critical_16", "icons/severity/critical_16.svg");
- registerIcon("high_16", "icons/severity/high_16.svg");
- registerIcon("medium_16", "icons/severity/medium_16.svg");
- registerIcon("low_16", "icons/severity/low_16.svg");
-
- // Register medium icons (20px)
- registerIcon("malicious_20", "icons/severity/malicious_20.svg");
- registerIcon("critical_20", "icons/severity/critical_20.svg");
- registerIcon("high_20", "icons/severity/high_20.svg");
- registerIcon("medium_20", "icons/severity/medium_20.svg");
- registerIcon("low_20", "icons/severity/low_20.svg");
-
- // Register base icons
+ // Register small icons (16px) - light and dark variants
+ registerIcon("malicious_16", "icons/severity_16/malicious.svg");
+ registerIcon("malicious_16_dark", "icons/severity_16/malicious_dark.svg");
+ registerIcon("critical_16", "icons/severity_16/critical.svg");
+ registerIcon("critical_16_dark", "icons/severity_16/critical_dark.svg");
+ registerIcon("high_16", "icons/severity_16/high.svg");
+ registerIcon("high_16_dark", "icons/severity_16/high_dark.svg");
+ registerIcon("medium_16", "icons/severity_16/medium.svg");
+ registerIcon("medium_16_dark", "icons/severity_16/medium_dark.svg");
+ registerIcon("low_16", "icons/severity_16/low.svg");
+ registerIcon("low_16_dark", "icons/severity_16/low_dark.svg");
+
+ // Register medium icons (20px) - light and dark variants
+ registerIcon("malicious_20", "icons/severity_20/malicious.svg");
+ registerIcon("malicious_20_dark", "icons/severity_20/malicious_dark.svg");
+ registerIcon("critical_20", "icons/severity_20/critical.svg");
+ registerIcon("critical_20_dark", "icons/severity_20/critical_dark.svg");
+ registerIcon("high_20", "icons/severity_20/high.svg");
+ registerIcon("high_20_dark", "icons/severity_20/high_dark.svg");
+ registerIcon("medium_20", "icons/severity_20/medium.svg");
+ registerIcon("medium_20_dark", "icons/severity_20/medium_dark.svg");
+ registerIcon("low_20", "icons/severity_20/low.svg");
+ registerIcon("low_20_dark", "icons/severity_20/low_dark.svg");
+
+ // Register base icons - light and dark variants
registerIcon("malicious", "icons/severity/malicious.svg");
+ registerIcon("malicious_dark", "icons/severity/malicious_dark.svg");
registerIcon("critical", "icons/severity/critical.svg");
+ registerIcon("critical_dark", "icons/severity/critical_dark.svg");
registerIcon("high", "icons/severity/high.svg");
+ registerIcon("high_dark", "icons/severity/high_dark.svg");
registerIcon("medium", "icons/severity/medium.svg");
+ registerIcon("medium_dark", "icons/severity/medium_dark.svg");
registerIcon("low", "icons/severity/low.svg");
+ registerIcon("low_dark", "icons/severity/low_dark.svg");
+
+ registerIcon("star_action", "icons/start-action.svg");
+ registerIcon("devassistBadge", "icons/devassist_badge.svg");
}
private static void registerIcon(String key, String path) {
- AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, path);
- imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, path));
+ // Load icons from devassist module instead of main plugin
+ imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin("com.checkmarx.eclipse.devassist", path));
}
/**
@@ -100,6 +118,29 @@ public static Image getIcon(String severity, Size size) {
return imageRegistry.get(key);
}
+ /**
+ * Get theme-aware icon for a severity level and size.
+ * Returns dark variant in dark theme, light variant in light theme.
+ *
+ * @param severity Severity level (case-insensitive)
+ * @param size Icon size
+ * @return Image instance or null if not found
+ */
+ public static Image getThemeAwareIcon(String severity, Size size) {
+ if (severity == null) {
+ return null;
+ }
+
+ String key = severity.toLowerCase() + size.getSuffix();
+
+ // Append _dark suffix if dark theme is active
+ if (DevAssistUtils.isDarkTheme()) {
+ key += "_dark";
+ }
+
+ return imageRegistry.get(key);
+ }
+
/**
* Get icon for a severity level with default small size.
*
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java
index c1ef416b..58621112 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java
@@ -16,10 +16,12 @@
public class SeverityImageComposer {
private static final Map compositeImageCache = new HashMap<>();
-
+
// Shared severity icon instances
- private static final Image MALICIOUS_ICON = IconRegistry.getIcon(DevAssistConstants.MALICIOUS, IconRegistry.Size.SMALL);
- private static final Image CRITICAL_ICON = IconRegistry.getIcon(DevAssistConstants.CRITICAL, IconRegistry.Size.SMALL);
+ private static final Image MALICIOUS_ICON = IconRegistry.getIcon(DevAssistConstants.MALICIOUS,
+ IconRegistry.Size.SMALL);
+ private static final Image CRITICAL_ICON = IconRegistry.getIcon(DevAssistConstants.CRITICAL,
+ IconRegistry.Size.SMALL);
private static final Image HIGH_ICON = IconRegistry.getIcon(DevAssistConstants.HIGH, IconRegistry.Size.SMALL);
private static final Image MEDIUM_ICON = IconRegistry.getIcon(DevAssistConstants.MEDIUM, IconRegistry.Size.SMALL);
private static final Image LOW_ICON = IconRegistry.getIcon(DevAssistConstants.LOW, IconRegistry.Size.SMALL);
@@ -33,7 +35,8 @@ public static Image createFullCompositeImage(FileNodeLabel fileNode) {
return null;
}
- // Create cache key with a prefix to avoid collisions with createSeverityBadgeImage
+ // Create cache key with a prefix to avoid collisions with
+ // createSeverityBadgeImage
String cacheKey = "full_" + createCacheKey(fileNode);
if (compositeImageCache.containsKey(cacheKey)) {
return compositeImageCache.get(cacheKey);
@@ -52,6 +55,7 @@ public static Image createFullCompositeImage(FileNodeLabel fileNode) {
return null;
}
}
+
/**
* Create a composite image showing severity icons with counts inline.
* Example: Creates visual badges for Critical:4, High:3, Medium:1
@@ -128,7 +132,7 @@ private static Image createBadgeImage(Display display, FileNodeLabel fileNode) {
int x = 0;
int y = 0;
-
+
if (hasCount(fileNode, "malicious") && MALICIOUS_ICON != null) {
gc.drawImage(MALICIOUS_ICON, x, y);
x += iconSize + spacing;
@@ -180,11 +184,16 @@ private static Image createFullBadgeImage(Display display, FileNodeLabel fileNod
// Count how many icons we need
int iconCount = 0;
- if (hasCount(fileNode, DevAssistConstants.MALICIOUS)) iconCount++;
- if (hasCount(fileNode, DevAssistConstants.CRITICAL)) iconCount++;
- if (hasCount(fileNode, DevAssistConstants.HIGH)) iconCount++;
- if (hasCount(fileNode, DevAssistConstants.MEDIUM)) iconCount++;
- if (hasCount(fileNode, DevAssistConstants.LOW)) iconCount++;
+ if (hasCount(fileNode, DevAssistConstants.MALICIOUS))
+ iconCount++;
+ if (hasCount(fileNode, DevAssistConstants.CRITICAL))
+ iconCount++;
+ if (hasCount(fileNode, DevAssistConstants.HIGH))
+ iconCount++;
+ if (hasCount(fileNode, DevAssistConstants.MEDIUM))
+ iconCount++;
+ if (hasCount(fileNode, DevAssistConstants.LOW))
+ iconCount++;
if (iconCount == 0) {
return null;
@@ -202,7 +211,7 @@ private static Image createFullBadgeImage(Display display, FileNodeLabel fileNod
int x = 0;
int y = 0;
-
+
if (hasCount(fileNode, DevAssistConstants.MALICIOUS) && MALICIOUS_ICON != null) {
gc.drawImage(MALICIOUS_ICON, x, y);
x += iconSize + spacing;
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java
index 322687df..d32d5f74 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java
@@ -1,15 +1,28 @@
package com.checkmarx.eclipse.devassist.ui.findings.marker;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.texteditor.ITextEditor;
import com.checkmarx.eclipse.common.enums.Severity;
import com.checkmarx.eclipse.devassist.model.Location;
import com.checkmarx.eclipse.devassist.model.ScanEngine;
import com.checkmarx.eclipse.devassist.model.ScanIssue;
-import org.eclipse.jface.text.Position;
-import org.eclipse.ui.texteditor.ITextEditor;
-
-import com.checkmarx.eclipse.devassist.problems.ProblemDecorator;
+import com.checkmarx.eclipse.devassist.model.Vulnerability;
/**
* Maps between ScanIssue objects and IMarker attributes.
@@ -18,6 +31,8 @@
*/
public class MarkerIssueMapper {
+ private static final String MARKER_TYPE = "com.checkmarx.eclipse.plugin.checkmarxProblemMarker";
+
// Marker attribute names (prefixed with cx. to avoid collision)
private static final String ATTR_ISSUE_ID = "cx.issueId";
private static final String ATTR_SEVERITY = "cx.severity";
@@ -27,6 +42,23 @@ public class MarkerIssueMapper {
private static final String ATTR_RULE_ID = "cx.ruleId";
private static final String ATTR_FILE_PATH = "cx.filePath";
public static final String ATTR_SCAN_ENGINE = "cx.scanEngine";
+ private static final String ATTR_VULNERABILITIES = "cx.vulnerabilities";
+
+ // Delimiters for the flat vulnerabilities encoding. These control characters
+ // (unit separator / record separator) can't legally appear in marker text
+ // (title/description), unlike printable characters such as commas or pipes.
+ private static final String VULN_FIELD_SEP = "";
+ private static final String VULN_RECORD_SEP = "";
+
+ /**
+ * Reads the Checkmarx issue id off a marker without needing a full
+ * fromMarker() reconstruction - used by the hover to cross-reference a
+ * MarkerAnnotation against an already-rendered FindingsAnnotation for the
+ * same underlying finding.
+ */
+ public static String getIssueId(IMarker marker) {
+ return marker.getAttribute(ATTR_ISSUE_ID, "");
+ }
/**
* Reconstruct a ScanIssue from marker attributes.
@@ -58,6 +90,7 @@ public static ScanIssue fromMarker(IMarker marker) {
int lineNumber = marker.getAttribute(IMarker.LINE_NUMBER, 1);
int charStart = marker.getAttribute(IMarker.CHAR_START, 0);
int charEnd = marker.getAttribute(IMarker.CHAR_END, 0);
+ String vulnerabilitiesRaw = marker.getAttribute(ATTR_VULNERABILITIES, "");
// Reconstruct ScanIssue
ScanIssue issue = new ScanIssue();
@@ -68,6 +101,9 @@ public static ScanIssue fromMarker(IMarker marker) {
issue.setRemediationAdvise(remediation);
issue.setRuleId(ruleId);
issue.setFilePath(filePath);
+ if (!vulnerabilitiesRaw.isEmpty()) {
+ issue.setVulnerabilities(decodeVulnerabilities(vulnerabilitiesRaw));
+ }
// Parse scan engine
try {
@@ -75,7 +111,6 @@ public static ScanIssue fromMarker(IMarker marker) {
} catch (IllegalArgumentException e) {
issue.setScanEngine(ScanEngine.ASCA);
}
-
// Reconstruct location
Location location = new Location();
location.setLine(lineNumber);
@@ -85,7 +120,6 @@ public static ScanIssue fromMarker(IMarker marker) {
return issue;
} catch (Exception e) {
-
e.printStackTrace();
return null;
}
@@ -96,9 +130,9 @@ public static ScanIssue fromMarker(IMarker marker) {
* Called when creating markers from findings.
*
* @param marker the IMarker to populate
- * @param issue the ScanIssue containing data to serialize
+ * @param issue the ScanIssue containing data to serialize
*/
- public static void populateMarker(IMarker marker, ScanIssue issue, ITextEditor editor) {
+ public static void populateMarker(IMarker marker, ScanIssue issue) {
try {
if (issue.getScanIssueId() != null && !issue.getScanIssueId().isEmpty()) {
marker.setAttribute(ATTR_ISSUE_ID, issue.getScanIssueId());
@@ -110,6 +144,7 @@ public static void populateMarker(IMarker marker, ScanIssue issue, ITextEditor e
if (issue.getTitle() != null && !issue.getTitle().isEmpty()) {
marker.setAttribute(ATTR_TITLE, issue.getTitle());
+ // Also set MESSAGE for default marker hover display
marker.setAttribute(IMarker.MESSAGE, issue.getTitle());
}
@@ -133,33 +168,296 @@ public static void populateMarker(IMarker marker, ScanIssue issue, ITextEditor e
marker.setAttribute(ATTR_SCAN_ENGINE, issue.getScanEngine().toString());
}
+ // Carry the full vulnerabilities list (ASCA/IAC can group several
+ // vulnerabilities under one issue) so marker-based hover/details
+ // reconstruction doesn't collapse back down to a single entry.
+ if (issue.getVulnerabilities() != null && !issue.getVulnerabilities().isEmpty()) {
+ marker.setAttribute(ATTR_VULNERABILITIES, encodeVulnerabilities(issue.getVulnerabilities()));
+ }
+
// Set standard marker attributes from location
if (issue.getLocations() != null && !issue.getLocations().isEmpty()) {
- Location location = issue.getLocations().get(0);
- marker.setAttribute(IMarker.LINE_NUMBER, location.getLine());
-
- if (editor != null) {
- // Use ProblemDecorator's calculateRange logic for accurate absolute offsets
- Position pos = ProblemDecorator.calculateRange(editor, issue);
- if (pos != null) {
- marker.setAttribute(IMarker.CHAR_START, pos.getOffset());
- marker.setAttribute(IMarker.CHAR_END, pos.getOffset() + pos.getLength());
- }
- } else {
- // Fallback when editor instance is unavailable
- marker.setAttribute(IMarker.CHAR_START, location.getStartIndex());
- marker.setAttribute(IMarker.CHAR_END, location.getEndIndex());
- }
+ applyLocationAttributes(marker, issue.getLocations().get(0));
+ // Calculate severity for Eclipse marker system (0=info, 1=warning, 2=error)
int severity = calculateMarkerSeverity(issue.getSeverity());
marker.setAttribute(IMarker.SEVERITY, severity);
}
-
} catch (Exception e) {
+
e.printStackTrace();
}
}
+ /**
+ * Ensures a {@value #MARKER_TYPE} marker exists for this finding, creating and
+ * populating
+ * one if none does yet. This is what CheckmarxMarkerResolutionGenerator's
+ * Ctrl+1/quick-fix-
+ * in-hover actions anchor to; ProblemDecorator calls this for every issue it
+ * decorates so the
+ * marker (and therefore the quick-fix actions) exists as soon as the squiggly
+ * does, instead of
+ * only after the user navigates to that specific finding from the Findings
+ * view.
+ *
+ * @param file the file the issue was found in
+ * @param issue the finding to ensure a marker for
+ */
+ public static void ensureMarker(IFile file, ScanIssue issue) {
+ if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) {
+ return;
+ }
+
+ try {
+ if (findMarker(file, issue) != null) {
+ return;
+ }
+ IMarker marker = file.createMarker(MARKER_TYPE);
+ int lineNumber = issue.getLocations().get(0).getLine();
+ marker.setAttribute(IMarker.LINE_NUMBER, lineNumber > 0 ? lineNumber : 1);
+ marker.setAttribute(IMarker.MESSAGE, issue.getTitle());
+ marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
+ marker.setAttribute(IMarker.USER_EDITABLE, false);
+
+ populateMarker(marker, issue);
+ } catch (Exception e) {
+ // Marker creation is best-effort: the squiggly annotation and
+ // CheckmarxAnnotationHover
+ // (both driven by the live ScanIssue/FindingsAnnotation, not this marker) still
+ // work
+ // even if this fails.
+ }
+ }
+
+ /**
+ * Finds the existing {@value #MARKER_TYPE} marker for a ScanIssue, matching by
+ * the stable
+ * scanIssueId when available and falling back to line+title for findings
+ * without one.
+ *
+ * @param file the file to search
+ * @param issue the finding to find a marker for
+ * @return the matching marker, or null if none exists
+ */
+ public static IMarker findMarker(IFile file, ScanIssue issue) {
+ if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) {
+ return null;
+ }
+
+ String issueId = issue.getScanIssueId();
+ int issueLine = issue.getLocations().get(0).getLine();
+ String issueTitle = issue.getTitle();
+
+ try {
+ IMarker[] markers = file.findMarkers(MARKER_TYPE, true, IResource.DEPTH_ZERO);
+ for (IMarker marker : markers) {
+ if (issueId != null && !issueId.isEmpty()) {
+ if (issueId.equals(marker.getAttribute(ATTR_ISSUE_ID, ""))) {
+ return marker;
+ }
+ continue;
+ }
+ // Fallback for findings without a scanIssueId: line+title heuristic.
+ int markerLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (markerLine == issueLine) {
+ String markerMsg = marker.getAttribute(IMarker.MESSAGE, "");
+ if (issueTitle == null || issueTitle.isEmpty() || markerMsg.contains(issueTitle)) {
+ return marker;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // fall through
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets IMarker.LINE_NUMBER and, when possible, IMarker.CHAR_START/CHAR_END from
+ * a
+ * Location. Most scan engines (OSS, IaC, Secrets, Containers) report
+ * startIndex/endIndex
+ * as offsets relative to the start of the line, not the file - writing them
+ * straight into
+ * CHAR_START/CHAR_END as absolute file offsets collapses every marker onto
+ * whichever line
+ * happens to contain that many characters (almost always line 1), independent
+ * of which
+ * line the finding is actually on. This resolves the line's real offset in the
+ * document and
+ * adds it in, mirroring the conversion ProblemDecorator already applies when
+ * positioning the
+ * squiggly annotation - so the IMarker (which is what Eclipse's built-in
+ * quick-fix-in-hover
+ * and Ctrl+1 machinery anchors to) lands on the same line as the squiggly
+ * instead of drifting
+ * to a different one.
+ *
+ * @param marker the IMarker being populated
+ * @param location the finding's location (line, and possibly line-relative or
+ * absolute start/end)
+ */
+ private static void applyLocationAttributes(IMarker marker, Location location) {
+ try {
+ marker.setAttribute(IMarker.LINE_NUMBER, location.getLine());
+ } catch (Exception e) {
+ return;
+ }
+
+ IDocument document = resolveDocument(marker);
+ if (document == null) {
+ // No open editor for this file (yet). Leave CHAR_START/CHAR_END unset rather
+ // than
+ // writing the scanner's raw, often line-relative, start/end indices in as if
+ // they
+ // were absolute file offsets - Eclipse falls back to deriving a position from
+ // LINE_NUMBER alone, which is still correct for the line even without a precise
+ // range.
+ return;
+ }
+
+ try {
+ int line = Math.max(0, location.getLine() - 1);
+ if (line >= document.getNumberOfLines()) {
+ return;
+ }
+
+ IRegion lineInfo = document.getLineInformation(line);
+ int lineOffset = lineInfo.getOffset();
+ int lineLength = lineInfo.getLength();
+ int docLength = document.getLength();
+
+ boolean isAbsoluteOffset = location.isAbsoluteOffset();
+ int charStart = isAbsoluteOffset ? location.getStartIndex() : (lineOffset + location.getStartIndex());
+ int charEnd = isAbsoluteOffset ? location.getEndIndex() : (lineOffset + location.getEndIndex());
+
+ // Scanners that don't report a real column range (e.g. ASCA only sets the line,
+ // leaving start/end at their default of 0) collapse to the very start of the
+ // line here -
+ // expand to the whole (leading-whitespace-trimmed) line instead of leaving a
+ // zero-length position, which some Eclipse annotation-model paths treat as
+ // invalid.
+ if (charStart <= lineOffset) {
+ charStart = lineOffset + getLeadingWhitespaceOffset(document, lineOffset, lineLength);
+ }
+ if (charEnd <= charStart) {
+ charEnd = lineOffset + lineLength;
+ }
+
+ charStart = Math.max(0, Math.min(charStart, docLength));
+ charEnd = Math.max(charStart, Math.min(charEnd, docLength));
+
+ marker.setAttribute(IMarker.CHAR_START, charStart);
+ marker.setAttribute(IMarker.CHAR_END, charEnd);
+ } catch (Exception e) {
+ // Leave CHAR_START/CHAR_END unset; the LINE_NUMBER set above still positions
+ // the
+ // marker on the correct line.
+ }
+ }
+
+ /**
+ * Finds the document for the marker's own file by searching every open editor
+ * reference
+ * across all workbench windows - not just the active editor - so markers
+ * created for a
+ * file that isn't currently focused (e.g. background/real-time scan results)
+ * still resolve
+ * to the right document instead of silently reading whichever file happens to
+ * be active.
+ * Returns null (rather than guessing) if the file has no open editor.
+ */
+ private static IDocument resolveDocument(IMarker marker) {
+ try {
+ IResource resource = marker.getResource();
+ if (!(resource instanceof IFile)) {
+ return null;
+ }
+ IFile file = (IFile) resource;
+
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ if (workbench == null) {
+ return null;
+ }
+
+ for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
+ IWorkbenchPage page = window.getActivePage();
+ if (page == null) {
+ continue;
+ }
+ for (IEditorReference ref : page.getEditorReferences()) {
+ IEditorPart editorPart = ref.getEditor(false);
+ if (editorPart == null) {
+ continue;
+ }
+ IEditorInput input = editorPart.getEditorInput();
+ if (!(input instanceof IFileEditorInput)
+ || !file.equals(((IFileEditorInput) input).getFile())) {
+ continue;
+ }
+ ITextEditor textEditor = editorPart.getAdapter(ITextEditor.class);
+ if (textEditor != null) {
+ return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
+ }
+ }
+ }
+ } catch (Exception e) {
+ // fall through
+ }
+ return null;
+ }
+
+ private static int getLeadingWhitespaceOffset(IDocument document, int lineOffset, int lineLength) {
+ try {
+ String lineText = document.get(lineOffset, lineLength);
+ int count = 0;
+ while (count < lineText.length() && Character.isWhitespace(lineText.charAt(count))) {
+ count++;
+ }
+ return count;
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Flattens title/description pairs into one marker-attribute-safe string.
+ */
+ private static String encodeVulnerabilities(List vulnerabilities) {
+ StringBuilder sb = new StringBuilder();
+ for (Vulnerability vuln : vulnerabilities) {
+ if (sb.length() > 0) {
+ sb.append(VULN_RECORD_SEP);
+ }
+ sb.append(sanitize(vuln.getTitle())).append(VULN_FIELD_SEP).append(sanitize(vuln.getDescription()));
+ }
+ return sb.toString();
+ }
+
+ private static List decodeVulnerabilities(String raw) {
+ List result = new ArrayList<>();
+ for (String record : raw.split(VULN_RECORD_SEP, -1)) {
+ if (record.isEmpty()) {
+ continue;
+ }
+ String[] fields = record.split(VULN_FIELD_SEP, -1);
+ Vulnerability vuln = new Vulnerability();
+ vuln.setTitle(fields.length > 0 ? fields[0] : "");
+ vuln.setDescription(fields.length > 1 ? fields[1] : "");
+ result.add(vuln);
+ }
+ return result;
+ }
+
+ private static String sanitize(String value) {
+ if (value == null) {
+ return "";
+ }
+ return value.replace(VULN_FIELD_SEP, " ").replace(VULN_RECORD_SEP, " ");
+ }
+
/**
* Convert Checkmarx severity to Eclipse marker severity level.
*/
@@ -169,8 +467,8 @@ private static int calculateMarkerSeverity(String severity) {
}
switch (severity.toLowerCase()) {
- case "critical":
case "malicious":
+ case "critical":
case "high":
return IMarker.SEVERITY_ERROR;
case "medium":
@@ -193,6 +491,7 @@ private static int toEclipseSeverity(Severity severity) {
switch (severity) {
case CRITICAL:
+ case MALICIOUS:
case HIGH:
return IMarker.SEVERITY_ERROR;
case MEDIUM:
@@ -204,4 +503,3 @@ private static int toEclipseSeverity(Severity severity) {
}
}
}
-
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java
index 3c2c63d6..060c70ac 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java
@@ -17,7 +17,7 @@ public ScanDetailWithPath(ScanIssue detail, String filePath, FileNodeLabel paren
this.filePath = filePath;
this.parentNode = parentNode;
}
-
+
public FileNodeLabel getParentNode() {
return parentNode;
}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java
index 442f8497..525d6c0c 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java
@@ -24,91 +24,91 @@
*/
public class FindingsContentProvider implements ITreeContentProvider {
- private final Map imageCache = new HashMap<>();
+ private final Map imageCache = new HashMap<>();
- @Override
- public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
- }
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
- @Override
+ @Override
public Object[] getElements(Object inputElement) {
- if (inputElement instanceof Map) {
- @SuppressWarnings("unchecked")
- Map> map = (Map>) inputElement;
- return map.entrySet().stream().map(entry -> {
- String fileName = getFileName(entry.getKey());
- Image fileIcon = getFileIcon(fileName);
- return new FileNodeLabel(fileName, entry.getKey(), entry.getValue(), fileIcon);
- }).toArray();
- }
- return new Object[0];
- }
-
- private Image getFileIcon(String fileName) {
- if (fileName == null || fileName.isEmpty()) {
- return null;
- }
-
- try {
- IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry();
- ImageDescriptor imageDescriptor = registry.getImageDescriptor(fileName);
-
- if (imageDescriptor != null) {
- return imageCache.computeIfAbsent(imageDescriptor, descriptor -> descriptor.createImage());
- }
- } catch (Exception e) {
- CxLogger.error("Error retrieving file icon for " + fileName, e);
- }
-
- return null;
- }
-
- @Override
- public Object[] getChildren(Object parentElement) {
- if (parentElement instanceof FileNodeLabel) {
- FileNodeLabel fileNode = (FileNodeLabel) parentElement;
- return fileNode.getIssues().stream()
- .map(issue -> new ScanDetailWithPath(issue, fileNode.getFilePath(), fileNode)).toArray();
- }
- return new Object[0];
- }
-
- @Override
- public Object getParent(Object element) {
- if (element instanceof ScanDetailWithPath) {
- return ((ScanDetailWithPath) element).getParentNode();
- }
- return null;
- }
-
- @Override
- public boolean hasChildren(Object element) {
- if (element instanceof FileNodeLabel) {
- return !((FileNodeLabel) element).getIssues().isEmpty();
- }
- return false;
- }
-
- private String getFileName(String filePath) {
- if (filePath == null || filePath.isEmpty()) {
- return "Unknown";
- }
- int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
- if (lastSeparator >= 0) {
- return filePath.substring(lastSeparator + 1);
- }
- return filePath;
- }
-
- @Override
- public void dispose() {
- // Dispose all cached native OS handles to prevent memory leaks
- for (Image image : imageCache.values()) {
- if (image != null && !image.isDisposed()) {
- image.dispose();
- }
- }
- imageCache.clear();
- }
+ if (inputElement instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map> map = (Map>) inputElement;
+ return map.entrySet().stream().map(entry -> {
+ String fileName = getFileName(entry.getKey());
+ Image fileIcon = getFileIcon(fileName);
+ return new FileNodeLabel(fileName, entry.getKey(), entry.getValue(), fileIcon);
+ }).toArray();
+ }
+ return new Object[0];
+ }
+
+ private Image getFileIcon(String fileName) {
+ if (fileName == null || fileName.isEmpty()) {
+ return null;
+ }
+
+ try {
+ IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry();
+ ImageDescriptor imageDescriptor = registry.getImageDescriptor(fileName);
+
+ if (imageDescriptor != null) {
+ return imageCache.computeIfAbsent(imageDescriptor, descriptor -> descriptor.createImage());
+ }
+ } catch (Exception e) {
+ CxLogger.error("Error retrieving file icon for " + fileName, e);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof FileNodeLabel) {
+ FileNodeLabel fileNode = (FileNodeLabel) parentElement;
+ return fileNode.getIssues().stream()
+ .map(issue -> new ScanDetailWithPath(issue, fileNode.getFilePath(), fileNode)).toArray();
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof ScanDetailWithPath) {
+ return ((ScanDetailWithPath) element).getParentNode();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof FileNodeLabel) {
+ return !((FileNodeLabel) element).getIssues().isEmpty();
+ }
+ return false;
+ }
+
+ private String getFileName(String filePath) {
+ if (filePath == null || filePath.isEmpty()) {
+ return "Unknown";
+ }
+ int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
+ if (lastSeparator >= 0) {
+ return filePath.substring(lastSeparator + 1);
+ }
+ return filePath;
+ }
+
+ @Override
+ public void dispose() {
+ // Dispose all cached native OS handles to prevent memory leaks
+ for (Image image : imageCache.values()) {
+ if (image != null && !image.isDisposed()) {
+ image.dispose();
+ }
+ }
+ imageCache.clear();
+ }
}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java
index 0d462a32..e336fdfc 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java
@@ -4,14 +4,16 @@
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.StyledString;
-import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Event;
import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel;
import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath;
+import com.checkmarx.eclipse.devassist.utils.DevAssistUtils;
import com.checkmarx.eclipse.devassist.model.ScanIssue;
import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry;
@@ -21,7 +23,7 @@
*/
public class FindingsLabelProvider extends DelegatingStyledCellLabelProvider {
- private static final String[] SEVERITIES = { "critical", "high", "medium", "low" };
+ private static final String[] SEVERITIES = { "malicious", "critical", "high", "medium", "low" };
private static final int BETWEEN_BADGE_SPACING = 4; // Space between different shield groups
private static final int TEXT_TO_BADGE_PADDING = 28; // Space after filename before first badge
@@ -43,7 +45,7 @@ public Image getImage(Object element) {
return ((FileNodeLabel) element).getIcon();
} else if (element instanceof ScanDetailWithPath) {
String severity = ((ScanDetailWithPath) element).getDetail().getSeverity();
- return IconRegistry.getIcon(severity, IconRegistry.Size.SMALL);
+ return IconRegistry.getThemeAwareIcon(severity, IconRegistry.Size.SMALL);
}
return null;
}
@@ -120,8 +122,8 @@ protected void paint(Event event, Object element) {
for (String severity : SEVERITIES) {
Long count = counts.get(severity);
if (count != null && count > 0) {
- // Grab actual shield PNG asset
- Image badgePng = IconRegistry.getIcon(severity, IconRegistry.Size.SMALL);
+ // Grab theme-aware shield icon (light or dark variant based on current theme)
+ Image badgePng = IconRegistry.getThemeAwareIcon(severity, IconRegistry.Size.MEDIUM);
if (badgePng != null) {
// Draw Shield Badge
@@ -131,18 +133,24 @@ protected void paint(Event event, Object element) {
// Draw Count Number tightly next to the shield
String countStr = String.valueOf(count);
- // Match text color dynamically (Use foreground selection color if item is highlighted)
+ // Set text color based on theme and selection state
if ((event.detail & SWT.SELECTED) != 0) {
event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT));
} else {
- event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_FOREGROUND));
+ // Use theme-based colors for non-selected state
+ if (DevAssistUtils.isDarkTheme()) {
+ event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_WHITE));
+ } else {
+ event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK));
+ }
}
// Make count text bold
- org.eclipse.swt.graphics.Font originalFont = event.gc.getFont();
- org.eclipse.swt.graphics.FontData[] fontData = originalFont.getFontData();
- for (org.eclipse.swt.graphics.FontData fd : fontData) {
+ Font originalFont = event.gc.getFont();
+ FontData[] fontData = originalFont.getFontData();
+ for (FontData fd : fontData) {
fd.setStyle(fd.getStyle() | SWT.BOLD);
+ fd.setHeight(9);
}
org.eclipse.swt.graphics.Font boldFont = new org.eclipse.swt.graphics.Font(event.display, fontData);
event.gc.setFont(boldFont);
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java
new file mode 100644
index 00000000..a7501a06
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java
@@ -0,0 +1,106 @@
+//package com.checkmarx.eclipse.devassist.ui.findings.realtime;
+//
+//import org.eclipse.core.resources.IFile;
+//import org.eclipse.jface.text.DocumentEvent;
+//import org.eclipse.jface.text.IDocumentListener;
+//
+//import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler;
+//
+///**
+// * Real-time document listener for Checkmarx scanning.
+// *
+// * Equivalent to JetBrains' LocalInspectionTool.buildVisitor() — detects when
+// * the user edits the currently opened file and triggers a real-time scan with
+// * debounce (1 second of inactivity).
+// *
+// * This listener observes every keystroke and delegates to DevAssistScanScheduler
+// * for debounced scanning coordination.
+// */
+//public class CheckmarxDocumentListener implements IDocumentListener {
+//
+// private final RealTimeScanJob scanJob;
+// private final IFile file;
+// private final String fileName;
+// private final DevAssistScanScheduler scheduler;
+// private volatile boolean skipNextChange = false;
+// private volatile long lastRescheduleTime = 0;
+//
+// /**
+// * Create a document listener for a specific file.
+// *
+// * @param fileName the name of the file being edited (for logging)
+// * @param scanJob the RealTimeScanJob to trigger on document changes
+// * @param file the IFile being edited
+// * @param scheduler the scheduler to coordinate scan rescheduling
+// */
+// public CheckmarxDocumentListener(String fileName, RealTimeScanJob scanJob, IFile file, DevAssistScanScheduler scheduler) {
+// this.fileName = fileName;
+// this.scanJob = scanJob;
+// this.file = file;
+// this.scheduler = scheduler;
+// }
+//
+// /**
+// * Called when the document is about to be changed.
+// * We don't need to do anything here, but we implement it for completeness.
+// */
+// @Override
+// public void documentAboutToBeChanged(DocumentEvent event) {
+// // No action needed before change
+// }
+//
+// /**
+// * Called when the document has been changed.
+// * Triggers the debounced real-time scan via DevAssistScanScheduler.
+// *
+// * This is equivalent to JetBrains' InspectionVisitor methods being called
+// * during AST traversal — every edit triggers a potential scan.
+// */
+// @Override
+// public void documentChanged(DocumentEvent event) {
+// try {
+// // Skip rescheduling if this is a programmatic change (e.g., annotation updates)
+// if (skipNextChange) {
+// skipNextChange = false;
+// return;
+// }
+//
+// // Prevent StackOverflowError from rapid recursive reschedules
+// long now = System.currentTimeMillis();
+// if (now - lastRescheduleTime < 100) {
+// return;
+// }
+// lastRescheduleTime = now;
+//
+// // Reschedule the debounced scan job via scheduler
+// // This cancels the previous job (if still scheduled) and starts a new 1-second timer
+// if (scheduler != null && file != null) {
+// scheduler.rescheduleInspection(file, 1000); // 1000ms = 1 second debounce
+// } else if (scanJob != null) {
+// // Fallback to direct reschedule if scheduler not available
+// scanJob.reschedule(1000);
+// }
+//
+// } catch (Exception e) {
+// e.printStackTrace();
+// }
+// }
+//
+// public void setSkipNextChange(boolean skip) {
+// this.skipNextChange = skip;
+// }
+//
+// /**
+// * Dispose this listener and clean up associated resources.
+// * Call this when the editor is closed.
+// */
+// public void dispose() {
+// if (scanJob != null) {
+// scanJob.cancel();
+// }
+// }
+//
+// public String getFileName() {
+// return fileName;
+// }
+//}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java
new file mode 100644
index 00000000..6f7fe659
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java
@@ -0,0 +1,408 @@
+//package com.checkmarx.eclipse.devassist.ui.findings.realtime;
+//
+//import org.eclipse.ui.IEditorPart;
+//import org.eclipse.ui.IPartListener2;
+//import org.eclipse.ui.IWorkbenchPartReference;
+//import org.eclipse.jface.text.IDocument;
+//import org.eclipse.jface.text.source.ISourceViewer;
+//import org.eclipse.ui.texteditor.ITextEditor;
+//import org.eclipse.core.runtime.ILog;
+//import org.eclipse.core.runtime.Platform;
+//import org.eclipse.core.runtime.Status;
+//
+//import java.util.HashMap;
+//import java.util.Map;
+//
+//import com.checkmarx.eclipse.devassist.problems.ProblemHolderService;
+//import com.checkmarx.eclipse.devassist.problems.ProblemDecorator;
+//
+///**
+// * Real-time editor listener for Checkmarx scanning.
+// *
+// * Equivalent to JetBrains' LocalInspectionTool integration — listens for editor
+// * open/close events and registers document listeners for real-time scanning.
+// *
+// * When a text editor opens:
+// * 1. Create a RealTimeScanJob for that file
+// * 2. Register a CheckmarxDocumentListener on the document
+// * 3. Every keystroke triggers the document listener
+// * 4. Document listener reschedules the job (1-second debounce)
+// * 5. When debounce expires, RealTimeScanJob.run() executes the scan
+// *
+// * When the editor closes:
+// * - Dispose of the document listener and cancel the job
+// */
+//public class CheckmarxEditorListener implements IPartListener2 {
+//
+// /**
+// * Map of documents to their associated listeners.
+// * Key: IDocument hash code (unique identifier for the document)
+// * Value: CheckmarxDocumentListener (for cleanup on editor close)
+// */
+// private final Map activeListeners = new HashMap<>();
+//
+// /**
+// * Map of documents to their associated scan jobs.
+// * Key: IDocument hash code
+// * Value: RealTimeScanJob (for cleanup and tracking)
+// */
+// private final Map activeScanJobs = new HashMap<>();
+//
+// public CheckmarxEditorListener() {
+//
+// }
+//
+// /**
+// * Get the Eclipse log for this plugin.
+// */
+// private ILog getLog() {
+// return Platform.getLog(getClass());
+// }
+//
+// /**
+// * Called when an editor part is opened.
+// * Register real-time scanning for this editor.
+// */
+// @Override
+// public void partOpened(IWorkbenchPartReference partRef) {
+// try {
+// Object part = partRef.getPart(false);
+// if (part instanceof IEditorPart) {
+// setupRealtimeScanning((IEditorPart) part);
+// }
+// } catch (Exception e) {
+// System.err.println("[REALTIME] Error in partOpened: " + e.getMessage());
+// e.printStackTrace();
+// }
+// }
+//
+// /**
+// * Called when an editor is activated.
+// * Setup scanning if not done, or trigger rescan if switching to an already-open tab.
+// */
+// @Override
+// public void partActivated(IWorkbenchPartReference partRef) {
+// try {
+// Object part = partRef.getPart(false);
+// if (part instanceof IEditorPart) {
+// IEditorPart editor = (IEditorPart) part;
+// IDocument document = getDocumentFromEditor(editor);
+// if (document != null) {
+// int documentId = document.hashCode();
+// // If already set up, trigger a rescan when user switches to tab
+// if (activeListeners.containsKey(documentId)) {
+// RealTimeScanJob scanJob = activeScanJobs.get(documentId);
+// if (scanJob != null) {
+//
+// scanJob.reschedule(0);
+// }
+// return;
+// }
+// }
+// // Not yet set up - do initial setup
+// setupRealtimeScanning(editor);
+// }
+// } catch (Exception e) {
+// System.err.println("[REALTIME] Error in partActivated: " + e.getMessage());
+// e.printStackTrace();
+// }
+// }
+//
+// /**
+// * Called when an editor is closed.
+// * Clean up document listeners and cancel pending scan jobs.
+// */
+// @Override
+// public void partClosed(IWorkbenchPartReference partRef) {
+// try {
+// Object part = partRef.getPart(false);
+// if (part instanceof IEditorPart) {
+// cleanupRealtimeScanning((IEditorPart) part);
+// }
+// } catch (Exception e) {
+// System.err.println("[REALTIME] Error in partClosed: " + e.getMessage());
+// e.printStackTrace();
+// }
+// }
+//
+// /**
+// * Setup real-time scanning on the given editor.
+// *
+// * @param editor the editor part (should be a text editor)
+// */
+// private void setupRealtimeScanning(IEditorPart editor) {
+// if (editor == null) {
+// return;
+// }
+//
+// // Get the document from the editor
+// IDocument document = getDocumentFromEditor(editor);
+// if (document == null) {
+// // Not a text editor or no document available
+// return;
+// }
+//
+// // Use document hash code as a unique identifier
+// int documentId = document.hashCode();
+//
+// // Check if we've already set up scanning for this document
+// if (activeListeners.containsKey(documentId)) {
+//
+// return;
+// }
+//
+// // Get file name for logging
+// String fileName = extractFileNameFromEditor(editor);
+//
+//
+// // Log to Eclipse Error Log
+// String message = "User opened the file: " + fileName;
+// getLog().log(new Status(Status.INFO, "com.checkmarx.eclipse.plugin", message));
+//
+// // Create a scan job for this file
+// // Note: We extract the IFile from the editor if possible, otherwise use null
+// // (The actual file can be obtained from the editor input)
+// org.eclipse.core.resources.IFile file = extractFileFromEditor(editor);
+// RealTimeScanJob scanJob = new RealTimeScanJob(file, fileName);
+//
+// // Get the scheduler from project session properties
+// com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = null;
+// if (file != null) {
+// try {
+// org.eclipse.core.resources.IProject project = file.getProject();
+// if (project != null) {
+// scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project.getSessionProperty(
+// new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "scan-scheduler"));
+// }
+// } catch (Exception e) {
+//
+// }
+// }
+//
+// // Create a document listener that will reschedule the job on every keystroke
+// CheckmarxDocumentListener docListener = new CheckmarxDocumentListener(fileName, scanJob, file, scheduler);
+//
+// // Register the document listener
+// try {
+// document.addDocumentListener(docListener);
+//
+// // Store the listener and job for later cleanup
+// activeListeners.put(documentId, docListener);
+// activeScanJobs.put(documentId, scanJob);
+//
+//
+//
+// // **CRITICAL FIX: Apply cached decorations if findings exist for this file**
+// // JetBrains pattern: when editor opens, apply cached decorations immediately
+// // This fixes the issue where decorations don't appear if editor wasn't open during scan
+// applyCachedDecorationsForFile(file, document);
+//
+// // **CRITICAL FIX: Trigger initial scan when file is opened**
+// // JetBrains pattern: scan on file open, then on keystroke debounce
+// // Without this, opening a file doesn't trigger any scan — only edits do
+//
+// scanJob.reschedule(0);
+//
+// } catch (Exception e) {
+// System.err.println("[REALTIME] ✗ Error registering document listener: " + e.getMessage());
+// e.printStackTrace();
+// }
+// }
+//
+// /**
+// * Cleanup real-time scanning on the given editor.
+// *
+// * @param editor the editor part being closed
+// */
+// private void cleanupRealtimeScanning(IEditorPart editor) {
+// if (editor == null) {
+// return;
+// }
+//
+// // Get the document from the editor
+// IDocument document = getDocumentFromEditor(editor);
+// if (document == null) {
+// return;
+// }
+//
+// int documentId = document.hashCode();
+//
+// // Remove the document listener
+// CheckmarxDocumentListener listener = activeListeners.remove(documentId);
+// if (listener != null) {
+// try {
+// document.removeDocumentListener(listener);
+// listener.dispose();
+//
+// } catch (Exception e) {
+// System.err.println("[REALTIME] Error removing document listener: " + e.getMessage());
+// }
+// }
+//
+// // Cancel the scan job
+// RealTimeScanJob scanJob = activeScanJobs.remove(documentId);
+// if (scanJob != null) {
+// scanJob.cancel();
+//
+// }
+// }
+//
+// /**
+// * Extract the IDocument from an editor.
+// * Handles both standard ITextEditor and editors like MavenPomEditor.
+// *
+// * @param editor the editor part
+// * @return the document, or null if not available
+// */
+// private IDocument getDocumentFromEditor(IEditorPart editor) {
+// if (editor == null) {
+// return null;
+// }
+//
+// // Try method 1: Direct ITextEditor instance
+// if (editor instanceof ITextEditor) {
+// ITextEditor textEditor = (ITextEditor) editor;
+// try {
+// return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
+// } catch (Exception e) {
+// // Fall through to try adapter pattern
+// }
+// }
+//
+// // Try method 2: Adapter pattern (for MavenPomEditor and other non-ITextEditor editors)
+// try {
+// ITextEditor textEditor = editor.getAdapter(ITextEditor.class);
+// if (textEditor != null) {
+// return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
+// }
+// } catch (Exception e) {
+// // Fall through to next method
+// }
+//
+// // Try method 3: Direct IDocument adapter (some editors provide this)
+// try {
+// IDocument document = editor.getAdapter(IDocument.class);
+// if (document != null) {
+// return document;
+// }
+// } catch (Exception e) {
+// // Fall through
+// }
+//
+// return null;
+// }
+//
+// /**
+// * Extract the file name from an editor for logging.
+// *
+// * @param editor the editor part
+// * @return the file name, or "unknown" if not available
+// */
+// private String extractFileNameFromEditor(IEditorPart editor) {
+// try {
+// return editor.getEditorInput().getName();
+// } catch (Exception e) {
+// return "unknown";
+// }
+// }
+//
+// /**
+// * Extract the IFile from an editor (may return null for non-workspace files).
+// *
+// * @param editor the editor part
+// * @return the IFile, or null if not available
+// */
+// private org.eclipse.core.resources.IFile extractFileFromEditor(IEditorPart editor) {
+// try {
+// if (editor.getEditorInput() instanceof org.eclipse.ui.part.FileEditorInput) {
+// org.eclipse.ui.part.FileEditorInput fileInput =
+// (org.eclipse.ui.part.FileEditorInput) editor.getEditorInput();
+// return fileInput.getFile();
+// }
+// } catch (Exception e) {
+// // Ignore exceptions; file extraction is optional
+// }
+// return null;
+// }
+//
+// /**
+// * Apply cached decorations (gutter icons, underlines) when editor opens.
+// *
+// * JetBrains pattern: when an editor opens, check if there are cached findings
+// * and apply decorations immediately. This ensures decorations appear even if
+// * the editor wasn't open when the scan completed.
+// *
+// * @param file the Eclipse IFile being opened
+// * @param document the document for the file
+// */
+// private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, IDocument document) {
+// if (file == null || document == null) {
+// return;
+// }
+//
+// try {
+// String filePath = file.getLocation().toOSString();
+// org.eclipse.core.resources.IProject project = file.getProject();
+//
+// if (project == null) {
+// return;
+// }
+//
+// // Get cached findings for this file
+// ProblemHolderService problemHolder =
+// (ProblemHolderService) project.getSessionProperty(
+// new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder"));
+//
+// if (problemHolder == null) {
+// return;
+// }
+//
+// java.util.List cachedIssues =
+// problemHolder.getScanIssuesByFile(filePath);
+//
+// if (cachedIssues == null || cachedIssues.isEmpty()) {
+//
+// return;
+// }
+//
+// // Apply decorations for cached findings
+//
+// ProblemDecorator.decorateEditor(file, cachedIssues);
+//
+// } catch (Exception e) {
+// System.err.println("[REALTIME] Error applying cached decorations: " + e.getMessage());
+// e.printStackTrace();
+// }
+// }
+//
+// // Implement other IPartListener2 methods (not used for real-time scanning)
+//
+// @Override
+// public void partBroughtToTop(IWorkbenchPartReference partRef) {}
+//
+// @Override
+// public void partDeactivated(IWorkbenchPartReference partRef) {}
+//
+// @Override
+// public void partHidden(IWorkbenchPartReference partRef) {}
+//
+// @Override
+// public void partVisible(IWorkbenchPartReference partRef) {}
+//
+// @Override
+// public void partInputChanged(IWorkbenchPartReference partRef) {}
+//
+// /**
+// * Get the number of active listeners (for testing/debugging).
+// */
+// public int getActiveListenerCount() {
+// return activeListeners.size();
+// }
+//
+// /**
+// * Get the number of active scan jobs (for testing/debugging).
+// */
+// public int getActiveScanJobCount() {
+// return activeScanJobs.size();
+// }
+//}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java
new file mode 100644
index 00000000..cb34ffc8
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java
@@ -0,0 +1,240 @@
+//package com.checkmarx.eclipse.devassist.ui.findings.realtime;
+//
+//import org.eclipse.core.resources.IFile;
+//import org.eclipse.core.runtime.IProgressMonitor;
+//import org.eclipse.core.runtime.IStatus;
+//import org.eclipse.core.runtime.Status;
+//import org.eclipse.core.runtime.jobs.Job;
+//import org.eclipse.core.runtime.ILog;
+//import org.eclipse.core.runtime.Platform;
+//
+///**
+// * Real-time scan job with debounce support.
+// *
+// * When the user edits a file, CheckmarxDocumentListener calls reschedule() repeatedly
+// * as the user types. This job cancels the previous scheduled execution and starts a
+// * new 1-second timer, so the scan only runs after the user pauses typing.
+// *
+// * Equivalent to:
+// * - JetBrains' real-time inspection pipeline (with debounce built-in)
+// * - Eclipse's incremental builder, but for on-demand scanning
+// *
+// * This is a background Job, so it runs off the UI thread and won't freeze the editor.
+// */
+//public class RealTimeScanJob extends Job {
+//
+// private final IFile file;
+// private final String fileName;
+//
+// // Store the timestamp when the user last made changes
+// private long lastChangeTime = System.currentTimeMillis();
+//
+// /**
+// * Create a real-time scan job for a specific file.
+// *
+// * @param file the IFile resource to scan
+// * @param fileName the file name (for logging)
+// */
+// public RealTimeScanJob(IFile file, String fileName) {
+// super("Checkmarx Real-Time Scan: " + fileName);
+// this.file = file;
+// this.fileName = fileName;
+//
+// // Configure job properties for background execution
+// setSystem(false); // Show in progress view
+// setPriority(Job.DECORATE); // Lower priority than user interactions
+// setUser(false); // Not a user-initiated job
+//
+//
+// }
+//
+// /**
+// * Get the Eclipse log for this plugin.
+// */
+// private ILog getLog() {
+// return Platform.getLog(getClass());
+// }
+//
+// /**
+// * Reschedule this job with a given delay (debounce).
+// *
+// * If the job is already scheduled, it is cancelled and rescheduled with a new delay.
+// * This ensures the scan only runs after the user stops typing for the specified delay.
+// *
+// * @param delayMs delay in milliseconds before the job should run
+// */
+// public synchronized void reschedule(long delayMs) {
+// // Update the last change time
+// this.lastChangeTime = System.currentTimeMillis();
+//
+// // Cancel any previously scheduled execution
+// cancel();
+//
+// // Schedule the job to run after the delay
+// schedule(delayMs);
+//
+//
+// }
+//
+// /**
+// * Run the real-time scan.
+// *
+// * This method is called by the Eclipse Jobs framework after the debounce delay expires.
+// * It performs the actual scanning logic.
+// *
+// * Currently, this just logs a message. In production, you would:
+// * 1. Parse the file
+// * 2. Run security checks (synchronously or via backend API)
+// * 3. Create markers for problems found
+// * 4. Update the editor decoration
+// *
+// * @param monitor progress monitor for cancellation support
+// * @return Status.OK if successful, Status.CANCEL if cancelled
+// */
+// @Override
+// protected IStatus run(IProgressMonitor monitor) {
+// try {
+// // Check if file still exists and is accessible
+// if (file == null || !file.exists()) {
+//
+// return Status.CANCEL_STATUS;
+// }
+//
+// // Check if the job was cancelled while waiting
+// if (monitor.isCanceled()) {
+//
+// return Status.CANCEL_STATUS;
+// }
+//
+// // **STEP 1: Check authentication status**
+// if (!isUserAuthenticated()) {
+//
+//
+// return Status.OK_STATUS; // Return OK but don't scan
+// }
+//
+//
+//
+//
+//
+//
+// // Call our backend scanners via ScanManager
+// try {
+// org.eclipse.core.resources.IProject project = file.getProject();
+// if (project == null || !project.isOpen()) {
+//
+// return Status.OK_STATUS;
+// }
+//
+// String projectName = project.getName();
+// org.eclipse.core.runtime.QualifiedName registryKey = new org.eclipse.core.runtime.QualifiedName(
+// "com.checkmarx.eclipse.plugin", "scanner-registry");
+// org.eclipse.core.runtime.QualifiedName stateHolderKey = new org.eclipse.core.runtime.QualifiedName(
+// "com.checkmarx.eclipse.plugin", "state-holder");
+//
+// // Get or lazily initialize backend services
+// com.checkmarx.eclipse.devassist.backend.ScannerRegistry registry =
+// (com.checkmarx.eclipse.devassist.backend.ScannerRegistry)
+// project.getSessionProperty(registryKey);
+//
+// com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder =
+// (com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder)
+// project.getSessionProperty(stateHolderKey);
+//
+// // Lazy initialization if not found
+// if (registry == null) {
+//
+// registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project);
+// project.setSessionProperty(registryKey, registry);
+//
+// }
+//
+// if (stateHolder == null) {
+//
+// stateHolder = new com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder();
+// project.setSessionProperty(stateHolderKey, stateHolder);
+//
+// }
+//
+// // Execute backend scanners
+//
+// com.checkmarx.eclipse.devassist.common.ScanManager scanManager =
+// new com.checkmarx.eclipse.devassist.common.ScanManager(registry, stateHolder);
+//
+// String filePath = file.getLocation().toOSString();
+//
+//
+// java.util.List issues =
+// scanManager.scanFile(filePath);
+//
+//
+// for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : issues) {
+// }
+//
+// // Publish results to UI
+//
+// if (!issues.isEmpty()) {
+// com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues);
+//
+// } else {
+//
+// }
+//
+// } catch (Exception e) {
+// System.err.println("[REALTIME] ✗ ERROR in step above: " + e.getMessage());
+// e.printStackTrace();
+// System.err.println("[REALTIME] Stack trace:");
+// for (StackTraceElement elem : e.getStackTrace()) {
+// System.err.println("[REALTIME] at " + elem);
+// }
+// }
+//
+//
+// return Status.OK_STATUS;
+//
+// } catch (Exception e) {
+// System.err.println("[REALTIME] ✗ UNEXPECTED ERROR during real-time scan: " + e.getMessage());
+// e.printStackTrace();
+// System.err.println("[REALTIME] Full stack trace:");
+// for (StackTraceElement elem : e.getStackTrace()) {
+// System.err.println("[REALTIME] at " + elem);
+// }
+// // Return error status but don't fail the job permanently
+// return new Status(IStatus.WARNING, "com.checkmarx.eclipse.plugin",
+// "Real-time scan failed for " + fileName, e);
+// }
+// }
+//
+// /**
+// * Check if user is authenticated by checking if API key is configured.
+// */
+// private boolean isUserAuthenticated() {
+// String apiKey = com.checkmarx.eclipse.common.properties.Preferences.getApiKey();
+// return apiKey != null && !apiKey.trim().isEmpty();
+// }
+//
+// @Override
+// public boolean belongsTo(Object family) {
+// // Group all Checkmarx real-time scan jobs together
+// // This allows Eclipse to cancel all scans at once if needed
+// return family != null && family.equals("com.checkmarx.realtime.scan");
+// }
+//
+// /**
+// * Called when the job is cancelled.
+// * Cleanup any resources if needed.
+// */
+// @Override
+// protected void canceling() {
+//
+// super.canceling();
+// }
+//
+// public String getFileName() {
+// return fileName;
+// }
+//
+// public IFile getFile() {
+// return file;
+// }
+//}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java
index 31584f54..340d05fe 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java
@@ -6,7 +6,8 @@
/**
* Provides marker resolutions for Checkmarx findings.
- * Invoked when user presses Ctrl+1 on a marker or selects "Quick Fix" from context menu.
+ * Invoked when user presses Ctrl+1 on a marker or selects "Quick Fix" from
+ * context menu.
* Implements IMarkerResolutionGenerator2 for efficient hasResolutions() check.
*/
public class CheckmarxMarkerResolutionGenerator implements IMarkerResolutionGenerator2 {
@@ -14,7 +15,10 @@ public class CheckmarxMarkerResolutionGenerator implements IMarkerResolutionGene
@Override
public IMarkerResolution[] getResolutions(IMarker marker) {
return new IMarkerResolution[] {
- new ViewFindingDetailsResolution(marker)
+ new QuickFixRemediationResolution(marker),
+ new ViewFindingDetailsResolution(marker),
+ new IgnoreVulnerabilityResolution(marker),
+ new CopyDetailsResolution(marker)
};
}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CopyDetailsResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CopyDetailsResolution.java
new file mode 100644
index 00000000..5a3c9003
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CopyDetailsResolution.java
@@ -0,0 +1,67 @@
+package com.checkmarx.eclipse.devassist.ui.findings.resolution;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IMarkerResolution2;
+
+import com.checkmarx.eclipse.common.utils.CxLogger;
+import com.checkmarx.eclipse.devassist.model.ScanIssue;
+import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper;
+import com.checkmarx.eclipse.devassist.utils.DevAssistConstants;
+
+/**
+ * Marker resolution that copies the finding's title and description to the clipboard.
+ * Implements IMarkerResolution2 for efficient hasResolutions() checks.
+ */
+public class CopyDetailsResolution implements IMarkerResolution2 {
+
+ private final Image icon;
+
+ public CopyDetailsResolution(IMarker marker) {
+ this.icon = ResolutionIconHelper.severityIconForMarker(marker);
+ }
+
+ @Override
+ public String getLabel() {
+ return DevAssistConstants.COPY_DETAILS_FIX_NAME;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Copy this finding's title and description to the clipboard";
+ }
+
+ @Override
+ public Image getImage() {
+ return icon;
+ }
+
+ @Override
+ public void run(IMarker marker) {
+ try {
+ ScanIssue issue = MarkerIssueMapper.fromMarker(marker);
+ if (issue == null) {
+ CxLogger.warning("CopyDetailsResolution: could not reconstruct ScanIssue from marker");
+ return;
+ }
+ String title = issue.getTitle() != null ? issue.getTitle() : "";
+ String description = issue.getDescription() != null ? issue.getDescription() : "";
+ String text = title + "\n" + description;
+
+ Display.getDefault().asyncExec(() -> {
+ Clipboard clipboard = new Clipboard(Display.getDefault());
+ try {
+ clipboard.setContents(new Object[] { text }, new Transfer[] { TextTransfer.getInstance() });
+ } finally {
+ clipboard.dispose();
+ }
+ });
+ } catch (Exception e) {
+ CxLogger.error("CopyDetailsResolution: failed to copy details", e);
+ }
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/IgnoreVulnerabilityResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/IgnoreVulnerabilityResolution.java
new file mode 100644
index 00000000..7c8f6818
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/IgnoreVulnerabilityResolution.java
@@ -0,0 +1,58 @@
+package com.checkmarx.eclipse.devassist.ui.findings.resolution;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.IMarkerResolution2;
+
+import com.checkmarx.eclipse.common.utils.CxLogger;
+import com.checkmarx.eclipse.devassist.model.ScanIssue;
+import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore;
+import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper;
+import com.checkmarx.eclipse.devassist.utils.DevAssistConstants;
+
+/**
+ * Marker resolution that marks a Checkmarx finding as ignored.
+ * Mirrors the JetBrains plugin's IgnoreVulnerabilityFix (LocalQuickFix) behavior.
+ * Deletes the marker after ignoring so the underline/gutter icon disappears immediately.
+ * Implements IMarkerResolution2 for efficient hasResolutions() checks.
+ */
+public class IgnoreVulnerabilityResolution implements IMarkerResolution2 {
+
+ private final Image icon;
+
+ public IgnoreVulnerabilityResolution(IMarker marker) {
+ this.icon = ResolutionIconHelper.severityIconForMarker(marker);
+ }
+
+ @Override
+ public String getLabel() {
+ return DevAssistConstants.IGNORE_THIS_VULNERABILITY_FIX_NAME;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Mark this Checkmarx finding as ignored";
+ }
+
+ @Override
+ public Image getImage() {
+ return icon;
+ }
+
+ @Override
+ public void run(IMarker marker) {
+ try {
+ ScanIssue issue = MarkerIssueMapper.fromMarker(marker);
+ if (issue == null) {
+ CxLogger.warning("IgnoreVulnerabilityResolution: could not reconstruct ScanIssue from marker");
+ return;
+ }
+ IgnoredProblemsStore.getInstance().ignoreProblem(issue);
+ if (marker.exists()) {
+ marker.delete();
+ }
+ } catch (Exception e) {
+ CxLogger.error("IgnoreVulnerabilityResolution: failed to ignore finding", e);
+ }
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java
new file mode 100644
index 00000000..6b1c200b
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java
@@ -0,0 +1,58 @@
+package com.checkmarx.eclipse.devassist.ui.findings.resolution;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.IMarkerResolution2;
+
+import com.checkmarx.eclipse.common.utils.CxLogger;
+import com.checkmarx.eclipse.devassist.model.ScanIssue;
+import com.checkmarx.eclipse.devassist.remediation.RemediationManager;
+import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry;
+import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper;
+import com.checkmarx.eclipse.devassist.utils.DevAssistConstants;
+
+import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.QUICK_FIX;
+
+/**
+ * Marker resolution that applies automated remediation for a Checkmarx finding.
+ * Mirrors the JetBrains plugin's DevAssistFix (LocalQuickFix) behavior:
+ * sends a remediation prompt to Copilot, falling back to clipboard copy.
+ * Implements IMarkerResolution2 for efficient hasResolutions() checks.
+ */
+public class QuickFixRemediationResolution implements IMarkerResolution2 {
+
+ private final Image icon;
+
+ public QuickFixRemediationResolution(IMarker marker) {
+ this.icon = ResolutionIconHelper.severityIconForMarker(marker);
+ }
+
+ @Override
+ public String getLabel() {
+ return DevAssistConstants.FIX_WITH_DEV_ASSIST;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Apply an automated fix for this Checkmarx finding";
+ }
+
+ @Override
+ public Image getImage() {
+ return icon;
+ }
+
+ @Override
+ public void run(IMarker marker) {
+ try {
+ ScanIssue issue = MarkerIssueMapper.fromMarker(marker);
+ if (issue == null) {
+ CxLogger.warning("QuickFixRemediationResolution: could not reconstruct ScanIssue from marker");
+ return;
+ }
+ new RemediationManager().fixWithCxOneAssist(issue, QUICK_FIX);
+ } catch (Exception e) {
+ CxLogger.error("QuickFixRemediationResolution: failed to apply remediation", e);
+ }
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java
new file mode 100644
index 00000000..6aa75821
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java
@@ -0,0 +1,40 @@
+package com.checkmarx.eclipse.devassist.ui.findings.resolution;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.graphics.Image;
+
+import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry;
+
+/**
+ * Shared helper for IMarkerResolution2 implementations to look up the
+ * severity icon for a Checkmarx marker, so all 4 Quick Fix actions for a
+ * given finding show the same severity-colored icon (reusing the existing
+ * IconRegistry SVG severity icons rather than introducing new action-specific
+ * icon assets).
+ */
+final class ResolutionIconHelper {
+
+ private static final String ATTR_SEVERITY = "cx.severity";
+
+ private ResolutionIconHelper() {
+ }
+
+ /**
+ * Reads the marker's stored severity attribute directly (without fully
+ * reconstructing a ScanIssue) and resolves it to a severity icon.
+ *
+ * @param marker the Checkmarx problem marker
+ * @return the severity Image, or null if unavailable/marker deleted
+ */
+ static Image severityIconForMarker(IMarker marker) {
+ try {
+ if (marker == null || !marker.exists()) {
+ return null;
+ }
+ String severity = marker.getAttribute(ATTR_SEVERITY, null);
+ return severity != null ? IconRegistry.getIcon(severity) : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java
index 7881693f..af9742af 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java
@@ -27,12 +27,15 @@
/**
* Marker resolution that opens a dialog showing complete finding details.
* Reconstructs ScanIssue from marker attributes and displays rich UI.
- * Implements IMarkerResolution2 for better performance with hasResolutions() check.
+ * Implements IMarkerResolution2 for better performance with hasResolutions()
+ * check.
*/
public class ViewFindingDetailsResolution implements IMarkerResolution2 {
+ private final Image icon;
+
public ViewFindingDetailsResolution(IMarker marker) {
- // Constructor parameter kept for instantiation, marker details retrieved from run() parameter
+ this.icon = ResolutionIconHelper.severityIconForMarker(marker);
}
@Override
@@ -47,8 +50,7 @@ public String getDescription() {
@Override
public Image getImage() {
- // Optional: Return an icon. For now, use default
- return null;
+ return icon;
}
@Override
@@ -57,21 +59,18 @@ public void run(IMarker marker) {
// Reconstruct ScanIssue from marker attributes
ScanIssue issue = MarkerIssueMapper.fromMarker(marker);
if (issue == null) {
-
+
return;
}
// Open the details dialog
FindingDetailsDialog dialog = new FindingDetailsDialog(
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
- issue
- );
+ issue);
dialog.open();
-
-
} catch (Exception e) {
-
+
e.printStackTrace();
}
}
@@ -103,8 +102,7 @@ protected void configureShell(Shell newShell) {
Point size = newShell.getSize();
newShell.setLocation(
bounds.x + (bounds.width - size.x) / 2,
- bounds.y + (bounds.height - size.y) / 2
- );
+ bounds.y + (bounds.height - size.y) / 2);
}
}
@@ -204,12 +202,12 @@ protected void createButtonsForButtonBar(Composite parent) {
}
private void onQuickFixClick() {
-
+
// TODO: Implement remediation integration
}
private void onIgnoreClick() {
-
+
// TODO: Implement ignore logic
}
@@ -223,12 +221,12 @@ private void onCopyClick() {
TextTransfer transfer = TextTransfer.getInstance();
clipboard.setContents(new Object[] { text }, new Transfer[] { transfer });
clipboard.dispose();
-
+
});
}
private void onOpenWindowClick() {
-
+
// TODO: Open Findings window and navigate to this issue
}
@@ -255,4 +253,3 @@ private String getSeverityText(String severity) {
}
}
}
-
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java
index f47e5d1d..74950313 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java
@@ -61,8 +61,7 @@ private DevAssistConstants() {
// ASCA Supported File Extensions
public static final List ASCA_SUPPORTED_EXTENSIONS = List.of(
- "java", "cs", "go", "py", "js", "jsx", "ts", "tsx", "rb", "cpp"
- );
+ "java", "cs", "go", "py", "js", "jsx", "ts", "tsx", "rb", "cpp");
// Dev Assist Fixes Constants
public static final String FIX_WITH_CXONE_ASSIST = "Fix with Checkmarx One Assist";
@@ -70,6 +69,7 @@ private DevAssistConstants() {
public static final String VIEW_DETAILS_FIX_NAME = "View details";
public static final String IGNORE_THIS_VULNERABILITY_FIX_NAME = "Ignore this vulnerability";
public static final String IGNORE_ALL_OF_THIS_TYPE_FIX_NAME = "Ignore all of this type";
+ public static final String COPY_DETAILS_FIX_NAME = "Copy finding details";
// Manifest file patterns
public static final List MANIFEST_FILE_PATTERNS = List.of(
@@ -104,8 +104,7 @@ private DevAssistConstants() {
"**/Gemfile.lock",
"**/cpanfile.snapshot",
"**/cpanfile",
- "**/pubspec.lock"
- );
+ "**/pubspec.lock");
// Container file patterns
public static final List CONTAINERS_FILE_PATTERNS = List.of(
@@ -115,19 +114,16 @@ private DevAssistConstants() {
"**/docker-compose.yml",
"**/docker-compose.yaml",
"**/docker-compose-*.yml",
- "**/docker-compose-*.yaml"
- );
+ "**/docker-compose-*.yaml");
// IaC file patterns and extensions
public static final List IAC_SUPPORTED_PATTERNS = List.of(
"**/dockerfile",
"**/*.auto.tfvars",
- "**/*.terraform.tfvars"
- );
+ "**/*.terraform.tfvars");
public static final List IAC_FILE_EXTENSIONS = List.of(
- "tf", "yaml", "yml", "json", "proto", "dockerfile"
- );
+ "tf", "yaml", "yml", "json", "proto", "dockerfile");
// Multiple issues on same line
public static final String MULTIPLE_IAC_ISSUES = " IAC issues detected on this line";
@@ -153,7 +149,7 @@ private DevAssistConstants() {
public static final String CX_AGENT_NAME = "Checkmarx One Assist";
public static final String CX_DEVASSIST_AGENT_NAME = "Checkmarx Developer Assist";
public static final List AI_AGENT_FILES = List.of("/Dummy.txt", "/", "/AIAssistantInput");
- public static final String SEPARATOR = ":";
+ public static final String SEPERATOR = ":";
public static final String QUICK_FIX = "QUICK_FIX";
public static final String UNDO = "Undo";
public static final String MALICIOUS = "malicious";
@@ -161,9 +157,10 @@ private DevAssistConstants() {
public static final String HIGH = "high";
public static final String MEDIUM = "medium";
public static final String LOW = "low";
-
-
- /******************************** WELCOME DIALOG ********************************/
+
+ /********************************
+ * WELCOME DIALOG
+ ********************************/
public static final String WELCOME_TITLE = "Welcome to Checkmarx";
public static final String WELCOME_SUBTITLE = "Checkmarx offers immediate threat detection and assists you in preventing vulnerabilities before they arise.";
public static final String WELCOME_ASSIST_TITLE = "Code Smarter with Checkmarx One Assist";
@@ -177,4 +174,32 @@ private DevAssistConstants() {
public static final String WELCOME_CLOSE_BUTTON = "Close";
public static final String WELCOME_MCP_INSTALLED_INFO = "Checkmarx MCP Installed automatically - no need for manual integration";
+ /**
+ * Constant class to hold image paths.
+ */
+ public static final class ImagePaths {
+
+ private ImagePaths() {
+ throw new UnsupportedOperationException("Cannot instantiate ImagePaths class");
+ }
+
+ public static final String DEV_ASSIST_PNG = "/icons/tooltip/cxone_assist.png";
+ public static final String CRITICAL_PNG = "/icons/tooltip/critical.png";
+ public static final String HIGH_PNG = "/icons/tooltip/high.png";
+ public static final String MEDIUM_PNG = "/icons/tooltip/medium.png";
+ public static final String LOW_PNG = "/icons/tooltip/low.png";
+ public static final String MALICIOUS_PNG = "/icons/tooltip/malicious.png";
+ public static final String PACKAGE_PNG = "/icons/tooltip/package.png";
+ public static final String CONTAINER_PNG = "/icons/tooltip/container.png";
+
+ // Vulnerability Severity Count Icons
+ public static final String CRITICAL_16_PNG = "/icons/tooltip/severity_count/critical.png";
+ public static final String HIGH_16_PNG = "/icons/tooltip/severity_count/high.png";
+ public static final String MEDIUM_16_PNG = "/icons/tooltip/severity_count/medium.png";
+ public static final String LOW_16_PNG = "/icons/tooltip/severity_count/low.png";
+
+ // DEVASSIST PLUGIN ICONS
+ public static final String DEVASSIST_BADGE_PNG = "/icons/tooltip/devassist_badge.png";
+ }
+
}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java
index 41f67e2b..d8d48717 100644
--- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java
@@ -5,7 +5,7 @@
import java.util.Base64;
import java.util.List;
import java.util.Objects;
-
+import java.net.URL;
import org.eclipse.core.resources.IFile;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jgit.annotations.NonNull;
@@ -18,6 +18,12 @@
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.e4.ui.css.swt.theme.ITheme;
+import org.eclipse.e4.ui.css.swt.theme.IThemeEngine;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
@@ -28,7 +34,8 @@
import com.checkmarx.eclipse.common.utils.CxLogger;
/**
- * Utility class for DevAssist operations. Provides methods for encoding, decoding,
+ * Utility class for DevAssist operations. Provides methods for encoding,
+ * decoding,
* severity normalization, and file type detection.
*/
public class DevAssistUtils {
@@ -37,6 +44,8 @@ public class DevAssistUtils {
public static final String DOCKERFILE = "dockerfile";
public static final String DOCKER_COMPOSE = "docker-compose";
public static final String HELM = "helm";
+ private static final String THEME_ENGINE_DISPLAY_KEY = "org.eclipse.e4.ui.css.swt.theme";
+ private static final String DARK_THEME_ID_FRAGMENT = "dark";
private DevAssistUtils() {
// Private constructor to prevent instantiation
@@ -107,24 +116,24 @@ public static String normalizeSeverity(String severity) {
}
String upper = severity.toUpperCase();
switch (upper) {
- case "MALICIOUS":
- return SeverityLevel.MALICIOUS.getSeverity();
- case "CRITICAL":
- return SeverityLevel.CRITICAL.getSeverity();
- case "HIGH":
- return SeverityLevel.HIGH.getSeverity();
- case "MEDIUM":
- return SeverityLevel.MEDIUM.getSeverity();
- case "LOW":
- return SeverityLevel.LOW.getSeverity();
- case "UNKNOWN":
- return SeverityLevel.UNKNOWN.getSeverity();
- case "OK":
- return SeverityLevel.OK.getSeverity();
- case "IGNORED":
- return SeverityLevel.IGNORED.getSeverity();
- default:
- return severity;
+ case "MALICIOUS":
+ return SeverityLevel.MALICIOUS.getSeverity();
+ case "CRITICAL":
+ return SeverityLevel.CRITICAL.getSeverity();
+ case "HIGH":
+ return SeverityLevel.HIGH.getSeverity();
+ case "MEDIUM":
+ return SeverityLevel.MEDIUM.getSeverity();
+ case "LOW":
+ return SeverityLevel.LOW.getSeverity();
+ case "UNKNOWN":
+ return SeverityLevel.UNKNOWN.getSeverity();
+ case "OK":
+ return SeverityLevel.OK.getSeverity();
+ case "IGNORED":
+ return SeverityLevel.IGNORED.getSeverity();
+ default:
+ return severity;
}
}
@@ -175,14 +184,15 @@ public static boolean isYamlFile(String filePath) {
}
String fileExtension = getFileExtension(filePath);
return Objects.nonNull(fileExtension)
- && DevAssistConstants.CONTAINER_HELM_EXTENSION.contains(fileExtension.toLowerCase());
+ && DevAssistConstants.CONTAINER_HELM_EXTENSION.contains(fileExtension.toLowerCase());
}
/**
* Extracts the file extension from a given file path string.
*
* @param filePath absolute or relative path to the file
- * @return lower-case extension without the leading dot, or null if no extension exists
+ * @return lower-case extension without the leading dot, or null if no extension
+ * exists
*/
public static String getFileExtension(String filePath) {
if (filePath == null || filePath.isBlank()) {
@@ -201,11 +211,13 @@ public static String getFileExtension(String filePath) {
* Get the live IDocument for a file if it is currently open in an editor.
*
* CRITICAL: Every scanner's scan(String filePath) previously passed a brand-new
- * empty Document, which forced getFileContent() to fall back to reading the file
+ * empty Document, which forced getFileContent() to fall back to reading the
+ * file
* from disk. This meant real-time scans always scanned the last SAVED content,
* never the current unsaved edit - causing results to lag one edit/save behind.
*
- * Runs the editor lookup on the UI thread (via syncExec) since scan() is invoked
+ * Runs the editor lookup on the UI thread (via syncExec) since scan() is
+ * invoked
* from a background Job thread and Workbench/editor APIs are not thread-safe.
*
* @param filePath Absolute OS file path to look up
@@ -252,7 +264,8 @@ public static IDocument getLiveDocumentForFile(String filePath) {
}
}
} catch (Exception e) {
- CxLogger.warning(LOG_TAG + " Error resolving live document for: " + filePath + " - " + e.getMessage());
+ CxLogger.warning(
+ LOG_TAG + " Error resolving live document for: " + filePath + " - " + e.getMessage());
}
});
} catch (Exception e) {
@@ -261,29 +274,31 @@ public static IDocument getLiveDocumentForFile(String filePath) {
return result[0];
}
-
+
public static String getAgentName() {
// TODO Auto-generated method stub
return DevAssistConstants.CX_AGENT_NAME;
}
+
/**
- * Returns the vulnerability details for the given vulnerability id.
- *
- * @param scanIssue scan issue containing vulnerabilities details
- * @param vulnerabilityId - vulnerability id
- * @return Vulnerability - vulnerability details
- */
- public static Vulnerability getVulnerabilityDetails(ScanIssue scanIssue, String vulnerabilityId) {
- if (Objects.isNull(scanIssue.getVulnerabilities()) || scanIssue.getVulnerabilities().isEmpty()) {
- CxLogger.warning(String.format("No vulnerabilities found in scan issue object for scan engine: %s.", scanIssue.getScanEngine().name()));
- return null;
- }
- return scanIssue.getVulnerabilities().stream()
- .filter(vulnerability -> vulnerability.getVulnerabilityId().equals(vulnerabilityId))
- .findFirst()
- .orElse(null);
- }
-
+ * Returns the vulnerability details for the given vulnerability id.
+ *
+ * @param scanIssue scan issue containing vulnerabilities details
+ * @param vulnerabilityId - vulnerability id
+ * @return Vulnerability - vulnerability details
+ */
+ public static Vulnerability getVulnerabilityDetails(ScanIssue scanIssue, String vulnerabilityId) {
+ if (Objects.isNull(scanIssue.getVulnerabilities()) || scanIssue.getVulnerabilities().isEmpty()) {
+ CxLogger.warning(String.format("No vulnerabilities found in scan issue object for scan engine: %s.",
+ scanIssue.getScanEngine().name()));
+ return null;
+ }
+ return scanIssue.getVulnerabilities().stream()
+ .filter(vulnerability -> vulnerability.getVulnerabilityId().equals(vulnerabilityId))
+ .findFirst()
+ .orElse(null);
+ }
+
/**
* Copies text to the system clipboard.
*
@@ -308,12 +323,11 @@ public static boolean copyToClipboard(String text) {
return false;
}
}
-
-
- /**
- * Copies the given text to the system clipboard and shows a standard
- * Eclipse notification popup confirming the action.
- */
+
+ /**
+ * Copies the given text to the system clipboard and shows a standard
+ * Eclipse notification popup confirming the action.
+ */
public static boolean copyToClipboardWithNotification(String notificationMessage, String notificationTitle) {
try {
Display display = Display.getCurrent() != null ? Display.getCurrent() : Display.getDefault();
@@ -339,5 +353,88 @@ public static boolean copyToClipboardWithNotification(String notificationMessage
return false;
}
}
-}
+ /**
+ * Get a Quick fix name for the quick fix action.
+ * Returns the appropriate fix name based on the plugin context.
+ * For Eclipse, defaults to DEV_ASSIST as this plugin is the DevAssist variant.
+ *
+ * @return Quick fix name string
+ */
+ public static String getAssistQuickFixName() {
+ return DevAssistConstants.FIX_WITH_DEV_ASSIST;
+ }
+
+ /**
+ * Returns a resource URL string suitable for embedding in an
+ * tag
+ * for the given simple icon key (e.g. "critical", "high", "package",
+ * "malicious").
+ *
+ * @param iconPath severity or logical icon path
+ * @return external form URL or empty string if not found
+ */
+ public static String themeBasedPNGIconForHtmlImage(String iconPath) {
+ if (iconPath == null || iconPath.isEmpty()) {
+ return "";
+ }
+ boolean dark = isDarkTheme();
+ String candidate = iconPath;
+ if (dark) {
+ int extensionIndex = iconPath.lastIndexOf(".png");
+ if (extensionIndex >= 0) {
+ candidate = iconPath.substring(0, extensionIndex) + "_dark" + iconPath.substring(extensionIndex);
+ } else {
+ candidate = iconPath + "_dark";
+ }
+ }
+ URL res = DevAssistUtils.class.getResource(candidate);
+ if (res == null && dark) {
+ // Fallback to the light variant
+ candidate = iconPath;
+ res = DevAssistUtils.class.getResource(candidate);
+ }
+ return res != null ? res.toExternalForm() : "";
+ }
+
+ /**
+ * Reads Eclipse's own e4 CSS theme engine - the same mechanism the Platform
+ * uses to decide dark vs. light styling - so the scanner image always matches
+ * whatever theme Eclipse is actually rendering with, instead of guessing from
+ * a color sample (which broke down in practice, e.g. custom/high-contrast
+ * themes).
+ */
+ public static boolean isDarkTheme() {
+ ITheme activeTheme = getActiveTheme();
+ if (activeTheme != null && activeTheme.getId() != null) {
+ return activeTheme.getId().toLowerCase().contains(DARK_THEME_ID_FRAGMENT);
+ }
+ return isDarkByBackgroundLuminance();
+ }
+
+ private static ITheme getActiveTheme() {
+ try {
+ Display display = Display.getCurrent();
+ Object engineData = display != null ? display.getData(THEME_ENGINE_DISPLAY_KEY) : null;
+ if (engineData instanceof IThemeEngine) {
+ return ((IThemeEngine) engineData).getActiveTheme();
+ }
+ } catch (Throwable t) {
+ // e4 CSS theming bundle not present/active in this runtime; caller falls back.
+ CxLogger.error("Eclipse e4 theme engine unavailable, falling back to color heuristic",
+ t instanceof Exception ? (Exception) t : new Exception(t));
+ }
+ return null;
+ }
+
+ /**
+ * Fallback for the rare runtime where the e4 CSS theme engine isn't registered
+ * on the Display: approximate dark mode from the widget background luminance.
+ */
+ private static boolean isDarkByBackgroundLuminance() {
+ Color background = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ double luminance = (0.299 * background.getRed() + 0.587 * background.getGreen() + 0.114 * background.getBlue())
+ / 255.0;
+ return luminance < 0.5;
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/HtmlEscapeUtil.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/HtmlEscapeUtil.java
new file mode 100644
index 00000000..34876abd
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/HtmlEscapeUtil.java
@@ -0,0 +1,19 @@
+package com.checkmarx.eclipse.devassist.utils;
+
+public final class HtmlEscapeUtil {
+
+ private HtmlEscapeUtil() {
+ }
+
+ public static String escape(String text) {
+ if (text == null) {
+ return "";
+ }
+ return text
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """)
+ .replace("'", "'");
+ }
+}
diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java
new file mode 100644
index 00000000..26086213
--- /dev/null
+++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java
@@ -0,0 +1,21 @@
+package com.checkmarx.eclipse.devassist.utils;
+
+/**
+ * Enumeration representing various scanning engines supported by the system.
+ * Each constant signifies a specific type of scanning capability provided by the platform.
+ *
+ * The available scanning engines are:
+ * - OSS: Represents scanning for Open Source Software dependencies and vulnerabilities.
+ * - SECRETS: Represents scanning for sensitive information such as secrets and credentials in the code.
+ * - CONTAINERS: Represents scanning for vulnerabilities in container images.
+ * - IAC: Represents scanning for Infrastructure as Code issues and misconfigurations.
+ * - ASCA: Represents scanning for Application Security Code Analysis.
+ */
+public enum ScanEngine {
+ OSS,
+ SECRETS,
+ CONTAINERS,
+ IAC,
+ ASCA,
+ ALL
+}