diff --git a/checkmarx-ast-eclipse-plugin-tests/.classpath b/checkmarx-ast-eclipse-plugin-tests/.classpath index 98ee5fe7..13b02eb1 100644 --- a/checkmarx-ast-eclipse-plugin-tests/.classpath +++ b/checkmarx-ast-eclipse-plugin-tests/.classpath @@ -1,8 +1,7 @@ - + - @@ -14,7 +13,8 @@ - + + diff --git a/checkmarx-ast-eclipse-plugin-tests/pom.xml b/checkmarx-ast-eclipse-plugin-tests/pom.xml index 94b4d65c..70e9bd00 100644 --- a/checkmarx-ast-eclipse-plugin-tests/pom.xml +++ b/checkmarx-ast-eclipse-plugin-tests/pom.xml @@ -43,35 +43,10 @@ XML CSV - HTML - - check - verify - check - - ${project.build.directory}/jacoco.exec - ${project.basedir}/../checkmarx-ast-eclipse-plugin/target/classes - - org/eclipse/wb/swt/SWTResourceManager.class - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 0.30 - - - - - - - + org.eclipse.tycho diff --git a/checkmarx-ast-eclipse-plugin/.classpath b/checkmarx-ast-eclipse-plugin/.classpath index 32e2245e..19a4d9ec 100644 --- a/checkmarx-ast-eclipse-plugin/.classpath +++ b/checkmarx-ast-eclipse-plugin/.classpath @@ -2,30 +2,26 @@ - + + + + + + - + + - - - - - - - - - - diff --git a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF index f9af0477..f8ed4a1d 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -5,9 +5,14 @@ Bundle-SymbolicName: com.checkmarx.eclipse.plugin;singleton:=true Bundle-Version: 1.0.0.qualifier Bundle-Vendor: Checkmarx Require-Bundle: org.eclipse.ui, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors, org.eclipse.core.runtime, org.eclipse.jdt.core, org.eclipse.ui.ide, + org.eclipse.jface.text, + org.eclipse.text, + org.eclipse.jdt.ui, org.eclipse.jgit, org.eclipse.e4.core.services, com.google.guava, @@ -22,6 +27,8 @@ Import-Package: org.eclipse.core.resources, org.osgi.service.event;version="1.4.1" Bundle-ActivationPolicy: lazy Bundle-Activator: com.checkmarx.eclipse.Activator +Export-Package: com.checkmarx.ast.asca, + com.checkmarx.ast.wrapper Bundle-ClassPath: ., lib/slf4j-simple-2.0.17.jar, lib/slf4j-reload4j-2.0.17.jar, diff --git a/checkmarx-ast-eclipse-plugin/build.properties b/checkmarx-ast-eclipse-plugin/build.properties index 680259c9..75590c4f 100644 --- a/checkmarx-ast-eclipse-plugin/build.properties +++ b/checkmarx-ast-eclipse-plugin/build.properties @@ -6,11 +6,11 @@ bin.includes = plugin.xml,\ lib/slf4j-reload4j-2.0.17.jar,\ lib/slf4j-api-2.0.17.jar,\ lib/jackson-annotations-2.21.jar,\ - lib/jackson-core-2.21.1.jar,\ + lib/jackson-core-2.21.4.jar,\ lib/commons-lang3-3.18.0.jar,\ lib/ast-cli-java-wrapper-2.4.24.jar,\ lib/org.eclipse.mylyn.commons.ui_4.9.0.v20251121-0615.jar,\ - lib/jackson-databind-2.21.1.jar,\ + lib/jackson-databind-2.21.5.jar,\ .,\ lib/org-eclipse-mylyn-commons-core.jar source.. = src/ diff --git a/checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png b/checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png new file mode 100644 index 00000000..e48df9b8 Binary files /dev/null and b/checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png differ diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg new file mode 100644 index 00000000..f3ab95d7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg new file mode 100644 index 00000000..6e1929e8 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg new file mode 100644 index 00000000..9c89888d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg new file mode 100644 index 00000000..9f6ad62b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high.svg new file mode 100644 index 00000000..3b3399b7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg new file mode 100644 index 00000000..4c815e84 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg new file mode 100644 index 00000000..d9b8a81f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg new file mode 100644 index 00000000..50b139fa --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg new file mode 100644 index 00000000..4ec04da0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg new file mode 100644 index 00000000..20246d56 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low.svg new file mode 100644 index 00000000..a429fd46 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg new file mode 100644 index 00000000..40b203e4 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg new file mode 100644 index 00000000..69f9b3a6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg new file mode 100644 index 00000000..5cb507fb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg new file mode 100644 index 00000000..db43abd1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg new file mode 100644 index 00000000..a004f117 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg new file mode 100644 index 00000000..3a6cda49 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg new file mode 100644 index 00000000..5be2c823 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg new file mode 100644 index 00000000..6cc09bf7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg new file mode 100644 index 00000000..dc746080 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg new file mode 100644 index 00000000..c139bab4 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/plugin.xml b/checkmarx-ast-eclipse-plugin/plugin.xml index e0f787b1..10f2d0e1 100644 --- a/checkmarx-ast-eclipse-plugin/plugin.xml +++ b/checkmarx-ast-eclipse-plugin/plugin.xml @@ -21,6 +21,15 @@ name="Checkmarx One Scan" restorable="true"> + + @@ -36,6 +45,12 @@ relationship="right" ratio="0.5"> + + @@ -43,4 +58,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java new file mode 100644 index 00000000..9cbe8d4b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java @@ -0,0 +1,21 @@ +package com.checkmarx.eclipse.devassist.backend; + +/** + * Constants for DevAssist backend operations. + * Mirrors JetBrains Constants pattern. + */ +public class Constants { + // Severity level string constants + public static final String MALICIOUS_SEVERITY = "Malicious"; + public static final String CRITICAL_SEVERITY = "Critical"; + public static final String HIGH_SEVERITY = "High"; + public static final String MEDIUM_SEVERITY = "Medium"; + public static final String LOW_SEVERITY = "Low"; + public static final String OK = "OK"; + public static final String UNKNOWN = "Unknown"; + public static final String IGNORE_LABEL = "Ignored"; + + private Constants() { + // Private constructor to prevent instantiation + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java new file mode 100644 index 00000000..c7e1265f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java @@ -0,0 +1,244 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.security.MessageDigest; + +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Tracks file modification state to prevent redundant scans. + * + * Stores a composite "state hash" for each file: + * - Document modification timestamp + * - File system last-modified time + * - Editor content hash + * + * When a file is requested for scanning, we compare the current state + * with the cached state. If unchanged, we skip the scan and return cached results. + * + * This mirrors the JetBrains DevAssistScanStateHolder pattern. + */ +public class DevAssistScanStateHolder { + + private static final String LOG_TAG = "[SCAN-STATE]"; + + private final ConcurrentHashMap fileStateHash = new ConcurrentHashMap<>(); + // Atomic in-flight marker to prevent concurrent scans of the same file + // putIfAbsent() detects if another thread is already scanning this file + private final ConcurrentHashMap inFlightScans = new ConcurrentHashMap<>(); + + /** + * Get the cached state hash for a file. + * + * @param filePath Absolute file path + * @return Last recorded state hash, or null if never scanned + */ + public Long getStateHash(String filePath) { + if (filePath == null) { + return null; + } + return fileStateHash.get(filePath); + } + + /** + * Update the state hash for a file (after successful scan). + * + * @param filePath Absolute file path + * @param stateHash New state hash + */ + public void updateStateHash(String filePath, long stateHash) { + if (filePath == null) { + return; + } + + Long previous = fileStateHash.put(filePath, stateHash); + CxLogger.info(LOG_TAG + " Updated state hash for: " + filePath + + " (previous: " + previous + ", new: " + stateHash + ")"); + } + + /** + * Check if a file has changed since last scan AND mark it as in-flight. + * CRITICAL: Uses atomic putIfAbsent() to prevent concurrent scans of the same file. + * If another thread is already scanning this file, returns false to skip duplicate work. + * + * @param filePath Absolute file path + * @param currentStateHash Current state of the file + * @return true if file changed AND no other scan is in-flight, false otherwise + */ + public boolean hasChanged(String filePath, long currentStateHash) { + if (filePath == null) { + return true; + } + + Long cachedHash = fileStateHash.get(filePath); + + // Never scanned before + if (cachedHash == null) { + CxLogger.info(LOG_TAG + " File never scanned: " + filePath); + // Atomic check: if another thread beat us here, skip to avoid duplicate work + if (inFlightScans.putIfAbsent(filePath, true) != null) { + CxLogger.info(LOG_TAG + " BLOCKED: Another scan already in-flight for: " + filePath); + return false; + } + return true; + } + + // Compare hashes + boolean changed = !cachedHash.equals(currentStateHash); + if (!changed) { + CxLogger.info(LOG_TAG + " File unchanged (cached): " + filePath); + return false; + } + + // File changed - atomically mark as in-flight to prevent duplicate concurrent scans + if (inFlightScans.putIfAbsent(filePath, true) != null) { + CxLogger.info(LOG_TAG + " BLOCKED: Another scan already in-flight for: " + filePath); + return false; + } + + return true; + } + + /** + * Mark a file scan as complete (remove in-flight marker). + * MUST be called after scan completes to unblock other threads. + * + * @param filePath Absolute file path + */ + public void markScanComplete(String filePath) { + if (filePath == null) { + return; + } + inFlightScans.remove(filePath); + } + + /** + * Clear state for a specific file (e.g., when file is deleted). + * Also clears any in-flight scan marker. + * + * @param filePath Absolute file path + */ + public void clearFileState(String filePath) { + if (filePath == null) { + return; + } + + fileStateHash.remove(filePath); + inFlightScans.remove(filePath); + CxLogger.info(LOG_TAG + " Cleared state for: " + filePath); + } + + /** + * Clear all state (on project close). + * Also clears all in-flight scan markers. + */ + public void clearAll() { + fileStateHash.clear(); + inFlightScans.clear(); + CxLogger.info(LOG_TAG + " All state cleared"); + } + + /** + * Compute a state hash for a file based on: + * - File system last modified time + * - Document content hash (if open in editor with unsaved changes) + * + * CRITICAL FIX: When file is dirty (unsaved), hash actual document content instead of + * using System.nanoTime(). Previous implementation returned different hash on every call, + * causing unnecessary rescans even when content didn't change. + * + * @param filePath File to hash + * @return Composite state hash + */ + public static long computeFileStateHash(String filePath) { + try { + java.nio.file.Path path = java.nio.file.Paths.get(filePath); + long fileModified = java.nio.file.Files.getLastModifiedTime(path).toMillis(); + + // Check if file is open in editor with unsaved changes + // If dirty (unsaved), hash actual document content to detect real changes + String dirtyDocumentContent = null; + try { + org.eclipse.ui.IWorkbench workbench = org.eclipse.ui.PlatformUI.getWorkbench(); + if (workbench != null && !workbench.isClosing()) { + for (org.eclipse.ui.IWorkbenchWindow window : workbench.getWorkbenchWindows()) { + for (org.eclipse.ui.IWorkbenchPage page : window.getPages()) { + for (org.eclipse.ui.IEditorReference ref : page.getEditorReferences()) { + org.eclipse.ui.IEditorPart editor = ref.getEditor(false); + if (editor != null && editor.isDirty()) { + try { + String editorPath = editor.getEditorInput().getAdapter(org.eclipse.core.resources.IFile.class) + .getLocation().toOSString(); + if (editorPath.equals(filePath)) { + // Get document content from editor + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider().getDocument(editor.getEditorInput()); + if (doc != null) { + dirtyDocumentContent = doc.get(); + break; + } + } + } + } catch (Exception e2) { + // Skip if we can't get editor or document + } + } + } + if (dirtyDocumentContent != null) break; + } + if (dirtyDocumentContent != null) break; + } + } + } catch (Exception e) { + // If workbench check fails, just use file timestamp + dirtyDocumentContent = null; + } + + // If file has unsaved changes, hash actual document content + // This ensures same content hashes to same value (no unnecessary rescans) + if (dirtyDocumentContent != null) { + return hashDocumentContent(dirtyDocumentContent); + } + + return fileModified; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error computing state hash: " + e.getMessage()); + return System.currentTimeMillis(); + } + } + + /** + * Compute SHA-256 hash of document content. + * CRITICAL: Enables stable hashing of dirty files - same content always produces same hash. + * + * @param content Document text content + * @return Long hash value (first 8 bytes of SHA-256) + */ + private static long hashDocumentContent(String content) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(content.getBytes("UTF-8")); + // Convert first 8 bytes to long + long result = 0; + for (int i = 0; i < 8; i++) { + result = (result << 8) | (hash[i] & 0xFF); + } + return result; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error hashing document content: " + e.getMessage()); + // Fallback to content length + hash code + return ((long) content.length() << 32) | (content.hashCode() & 0xFFFFFFFFL); + } + } + + /** + * Get statistics about tracked files. + * + * @return Summary string + */ + public String getStatistics() { + return "Tracked files: " + fileStateHash.size(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java new file mode 100644 index 00000000..ab971710 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java @@ -0,0 +1,209 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Application-level singleton managing global scanner state. + * + * Responsibilities: + * - Track which scanners are enabled/disabled globally + * - Sync with user preferences/settings + * - Notify all open projects when scanner state changes + * - Provide query methods for scanner availability + * + * This is an application-scoped service (one instance for entire Eclipse). + * Each project's ScannerRegistry checks this controller before executing scans. + * + * Mirrors the JetBrains GlobalScannerController pattern. + */ +public class GlobalScannerController { + + private static final String LOG_TAG = "[GLOBAL-SCANNER]"; + private static GlobalScannerController instance; + + // Global enable/disable state for each scanner + private final ConcurrentHashMap scannerState = + new ConcurrentHashMap<>(); + + // Listeners notified when scanner state changes + private final List stateListeners = new ArrayList<>(); + + /** + * Get the global singleton instance. + * Lazily creates on first access. + * + * @return Global scanner controller + */ + public synchronized static GlobalScannerController getInstance() { + if (instance == null) { + instance = new GlobalScannerController(); + } + return instance; + } + + /** + * Enable a scanner globally. + * + * @param type Scanner type to enable + */ + public void enableScanner(ScannerType type) { + if (type == null) { + return; + } + + boolean wasDisabled = Boolean.FALSE.equals(scannerState.put(type, true)); + + if (wasDisabled) { + CxLogger.info(LOG_TAG + " Enabled scanner: " + type.getDisplayName()); + notifyScannerStateChanged(type, true); + } + } + + /** + * Disable a scanner globally. + * + * @param type Scanner type to disable + */ + public void disableScanner(ScannerType type) { + if (type == null) { + return; + } + + boolean wasEnabled = Boolean.TRUE.equals(scannerState.put(type, false)); + + if (wasEnabled) { + CxLogger.info(LOG_TAG + " Disabled scanner: " + type.getDisplayName()); + notifyScannerStateChanged(type, false); + } + } + + /** + * Check if a scanner is enabled globally. + * + * @param type Scanner type to check + * @return true if enabled, false if disabled + */ + public boolean isScannerEnabled(ScannerType type) { + if (type == null) { + return false; + } + + // Default to enabled if not explicitly set + return scannerState.getOrDefault(type, true); + } + + /** + * Enable all scanners. + */ + public void enableAllScanners() { + CxLogger.info(LOG_TAG + " Enabling all scanners"); + + for (ScannerType type : ScannerType.values()) { + enableScanner(type); + } + } + + /** + * Disable all scanners. + */ + public void disableAllScanners() { + CxLogger.info(LOG_TAG + " Disabling all scanners"); + + for (ScannerType type : ScannerType.values()) { + disableScanner(type); + } + } + + /** + * Get count of enabled scanners. + * + * @return Number of enabled scanners + */ + public int getEnabledScannerCount() { + int count = 0; + for (ScannerType type : ScannerType.values()) { + if (isScannerEnabled(type)) { + count++; + } + } + return count; + } + + /** + * Register a listener to be notified of state changes. + * + * @param listener Listener callback + */ + public void addScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.add(listener); + } + } + + /** + * Unregister a state listener. + * + * @param listener Listener to remove + */ + public void removeScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.remove(listener); + } + } + + /** + * Notify all listeners of a scanner state change. + * + * @param type Changed scanner type + * @param enabled New enabled state + */ + private void notifyScannerStateChanged(ScannerType type, boolean enabled) { + for (ScannerStateListener listener : stateListeners) { + try { + listener.onScannerStateChanged(type, enabled); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error notifying listener: " + e.getMessage()); + } + } + } + + /** + * Get a detailed state report. + * + * @return Multi-line status string + */ + public String getStateReport() { + StringBuilder sb = new StringBuilder(); + sb.append(LOG_TAG).append(" Scanner State Report:\n"); + + for (ScannerType type : ScannerType.values()) { + boolean enabled = isScannerEnabled(type); + sb.append(" ").append(type.getDisplayName()).append(": ") + .append(enabled ? "ENABLED" : "DISABLED").append("\n"); + } + + sb.append(" Total Enabled: ").append(getEnabledScannerCount()).append("/") + .append(ScannerType.values().length); + + return sb.toString(); + } + + /** + * Listener interface for scanner state changes. + * Implemented by project registries to react to global changes. + */ + public interface ScannerStateListener { + /** + * Called when a scanner's enabled state changes globally. + * + * @param type Changed scanner type + * @param enabled New enabled state + */ + void onScannerStateChanged(ScannerType type, boolean enabled); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java new file mode 100644 index 00000000..8d624232 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -0,0 +1,335 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IProject; + +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; + +/** + * Manages the lifecycle of scanner services for a project. + * + * Responsibilities: + * - Create scanner instances when project opens + * - Store scanner instances for reuse + * - Dispose scanners when project closes + * + * This is a project-level service. Each open project gets its own registry. + * Scanners are lazily initialized on first access. + * + * Mirrors the JetBrains ScannerRegistry pattern. + */ +public class ScannerRegistry { + + private static final String LOG_TAG = "[SCANNER-REGISTRY]"; + + // Session property key for storing registry on project + public static final String REGISTRY_KEY = ScannerRegistry.class.getName() + ".INSTANCE"; + + private final IProject project; + private final ConcurrentHashMap scanners = new ConcurrentHashMap<>(); + private boolean disposed = false; + + /** + * Create a registry for a project. + * + * @param project Eclipse project + */ + public ScannerRegistry(IProject project) { + this.project = project; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + + /** + * Deregister and dispose all scanners (on project close). + */ + public void deregisterAllScanners() { + CxLogger.info(LOG_TAG + " Deregistering all scanners for: " + project.getName()); + + // Dispose each scanner + scanners.forEach((type, scanner) -> { + try { + if (scanner instanceof AutoCloseable) { + ((AutoCloseable) scanner).close(); + } + CxLogger.info(LOG_TAG + "Disposed scanner: " + type); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing scanner " + type + ": " + + e.getMessage()); + } + }); + + scanners.clear(); + disposed = true; + CxLogger.info(LOG_TAG + " All scanners disposed"); + } + + /** + * Get a scanner service by type. + * Lazily creates the scanner on first access. + * + * @param type Scanner type (OSS, SECRETS, etc.) + * @return Scanner instance, or null if scanner type not supported + */ + public Object getScannerService(ScannerType type) { + if (disposed) { + CxLogger.warning(LOG_TAG + " Registry is disposed"); + return null; + } + + return scanners.computeIfAbsent(type.name(), key -> { + CxLogger.info(LOG_TAG + " Creating scanner: " + type); + // Scanner creation will be implemented in Phase 2 + return createScannerInstance(type); + }); + } + + /** + * Create a scanner instance by type. + * Creates implementations of ScannerService that delegate to the new scanner commands. + * + * @param type Scanner type + * @return Scanner instance + */ + private Object createScannerInstance(ScannerType type) { + try { + CxLogger.info(LOG_TAG + " Creating scanner instance for: " + type.getDisplayName()); + Object scanner = null; + + switch (type) { + case OSS: + scanner = new OssScannerServiceImpl(project); + break; + case SECRETS: + scanner = new SecretsScannerServiceImpl(project); + break; + case CONTAINERS: + scanner = new ContainerScannerServiceImpl(project); + break; + case IAC: + scanner = new IacScannerServiceImpl(project); + break; + case ASCA: + scanner = new AscaScannerServiceImpl(project); + break; + default: + return null; + } + + if (scanner != null) { + CxLogger.info(LOG_TAG + "Successfully created scanner: " + type.getDisplayName()); + } else { + CxLogger.warning(LOG_TAG + "Scanner returned null: " + type.getDisplayName()); + } + return scanner; + } catch (Exception e) { + CxLogger.error(LOG_TAG + "Error creating scanner " + type.getDisplayName() + ": " + e.getMessage(), e); + e.printStackTrace(); + return null; + } + } + + /** + * Inner class implementations of ScannerService that bridge to new scanner commands. + * These are minimal adapters that delegate to the proper scanner packages. + */ + + private static class OssScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand command; + private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + OssScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand(project); + this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() + .engineName("OSS") + .build(); + } + @Override + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[OSS-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } + } + @Override + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class SecretsScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand command; + private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + SecretsScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand(project); + this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() + .engineName("SECRETS") + .build(); + } + @Override + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[SECRETS-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } + } + @Override + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class IacScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand command; + private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + IacScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand(project); + this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() + .engineName("IAC") + .build(); + } + @Override + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[IAC-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } + } + @Override + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class AscaScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand command; + private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + AscaScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand(project); + this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() + .engineName("ASCA") + .build(); + } + @Override + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[ASCA-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } + } + @Override + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class ContainerScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand command; + private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + ContainerScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand(project); + this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() + .engineName("CONTAINERS") + .build(); + } + @Override + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[CONTAINER-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } + } + @Override + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + @Override + public void close() throws Exception { command.dispose(); } + } + + /** + * Check if a scanner is registered. + * + * @param type Scanner type + * @return true if scanner exists + */ + public boolean hasScannerService(ScannerType type) { + return scanners.containsKey(type.name()); + } + + /** + * Get the project this registry belongs to. + * + * @return Eclipse project + */ + public IProject getProject() { + return project; + } + + /** + * Check if registry is disposed. + * + * @return true if disposed + */ + public boolean isDisposed() { + return disposed; + } + + /** + * Get statistics for debugging. + * + * @return Summary string + */ + public String getStatistics() { + return "Project: " + project.getName() + + ", Scanners: " + scanners.size() + + ", Disposed: " + disposed; + } + + /** + * Enum of available scanner types. + * Maps to the 5 scanner engines in Checkmarx. + */ + public enum ScannerType { + OSS("Open Source Supply Chain"), + SECRETS("Secrets Scanning"), + CONTAINERS("Container Scanning"), + IAC("Infrastructure as Code"), + ASCA("Application Security Code Analysis"); + + private final String displayName; + + ScannerType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java new file mode 100644 index 00000000..459da6f8 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java @@ -0,0 +1,51 @@ +package com.checkmarx.eclipse.devassist.backend; + +/** + * Severity level enumeration matching JetBrains implementation. + * Provides 8 severity levels with precedence values (lower = more severe). + */ +public enum SeverityLevel { + MALICIOUS("Malicious", 1), + CRITICAL("Critical", 2), + HIGH("High", 3), + MEDIUM("Medium", 4), + LOW("Low", 5), + UNKNOWN("Unknown", 6), + OK("OK", 7), + IGNORED("Ignored", 8); + + private final String severity; + private final int precedence; + + SeverityLevel(String severity, int precedence) { + this.severity = severity; + this.precedence = precedence; + } + + public String getSeverity() { + return severity; + } + + public int getPrecedence() { + return precedence; + } + + /** + * Convert string severity value to enum. + * Returns UNKNOWN if no match found. + * + * @param value Severity string (case-insensitive) + * @return Matching SeverityLevel or UNKNOWN + */ + public static SeverityLevel fromValue(String value) { + if (value == null) { + return UNKNOWN; + } + for (SeverityLevel level : values()) { + if (level.getSeverity().equalsIgnoreCase(value)) { + return level; + } + } + return UNKNOWN; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java new file mode 100644 index 00000000..9049e681 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -0,0 +1,344 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.result.ResultPublisher; +import com.checkmarx.eclipse.devassist.common.ScanManager; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +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; + +public class ProjectLifecycleListener implements IResourceChangeListener { + + private static final String LOG_TAG = "[PROJECT-LISTENER]"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + + private static final QualifiedName REGISTRY_KEY = new QualifiedName(PLUGIN_ID, "scanner-registry"); + private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); + private static final QualifiedName STATE_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "state-holder"); + private static final QualifiedName WORKSPACE_SCAN_JOB_KEY = new QualifiedName(PLUGIN_ID, "workspace-scan-job"); + + private final List initializedProjects = new ArrayList<>(); + + /** + * Register this listener with Eclipse workspace and process existing open projects. + */ + public void register() { + CxLogger.info(LOG_TAG + " Registering project lifecycle listener"); + ResourcesPlugin.getWorkspace().addResourceChangeListener( + this, + IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE + ); + CxLogger.info(LOG_TAG + " ✓ Registered"); + + // FIX 1: Run immediate initialization for projects ALREADY open on IDE startup + initExistingProjects(); + } + + /** + * Re-runs initialization (registry setup + initial OSS/IaC/container scan) for + * any already-open projects that were skipped earlier because the user wasn't + * authenticated yet - the exact same path {@link #register()} runs for + * already-open projects at plugin launch. onProjectOpen() only proceeds when + * isUserAuthenticated() is true and nothing else ever re-triggers it for a + * project that was already open (only a real open/close event does), so a + * login that happens after Eclipse already started needs to call this to get + * the same initial scan that a fresh launch would have performed. + */ + public void scanAlreadyOpenProjects() { + initExistingProjects(); + } + + /** + * Scans the workspace and initializes any projects that are already open. + */ + private void initExistingProjects() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (IProject project : projects) { + if (project.isOpen() && !isInitialized(project)) { + + onProjectOpen(project); + } + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error initializing existing projects on startup: " + e.getMessage(), e); + } + } + + public void unregister() { + CxLogger.info(LOG_TAG + " Unregistering project lifecycle listener"); + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + } + + /** + * Handle resource change events for project state changes (open/close). + */ + @Override + public void resourceChanged(IResourceChangeEvent event) { + try { + // Handle project close (PRE_CLOSE) + if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + IResource resource = event.getResource(); + if (resource instanceof IProject) { + onProjectClose((IProject) resource); + } + return; + } + + // FIX 2: Inspect IResourceDelta to catch when a closed project is opened manually + if (event.getType() == IResourceChangeEvent.POST_CHANGE && event.getDelta() != null) { + event.getDelta().accept(delta -> { + IResource resource = delta.getResource(); + if (resource instanceof IProject) { + IProject project = (IProject) resource; + // Check if project OPEN state changed + if ((delta.getFlags() & IResourceDelta.OPEN) != 0) { + if (project.isOpen() && !isInitialized(project)) { + onProjectOpen(project); + } else if (!project.isOpen() && isInitialized(project)) { + onProjectClose(project); + } + } + } + // Only visit top-level delta children (projects are at root level) + return true; + }); + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error handling resource change: " + e.getMessage(), e); + } + } + + private void onProjectOpen(IProject project) { + String projName = project.getName(); + if (projName.length() > 26) projName = projName.substring(0, 26); + try { + if (!isUserAuthenticated()) { + return; + } + + ScannerRegistry registry = new ScannerRegistry(project); + project.setSessionProperty(REGISTRY_KEY, registry); + + ProblemHolderService problemHolder = new ProblemHolderService(); + project.setSessionProperty(PROBLEM_HOLDER_KEY, problemHolder); + DevAssistScanStateHolder stateHolder = new DevAssistScanStateHolder(); + project.setSessionProperty(STATE_HOLDER_KEY, stateHolder); + initializedProjects.add(project.getName()); + + startWorkspaceFileScanning(project); + + } catch (Exception e) { + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error initializing project " + + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isUserAuthenticated() { + String apiKey = com.checkmarx.eclipse.properties.Preferences.getApiKey(); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + private void onProjectClose(IProject project) { + CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); + + try { + // Cancel any in-flight workspace scan job + try { + Job scanJob = (Job) project.getSessionProperty(WORKSPACE_SCAN_JOB_KEY); + if (scanJob != null && scanJob.getState() != Job.NONE) { + scanJob.cancel(); + CxLogger.info(LOG_TAG + " ✓ Cancelled workspace scan job for " + project.getName()); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error cancelling scan job: " + e.getMessage()); + } + + try { + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); + if (registry != null) { + registry.deregisterAllScanners(); + CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing ScannerRegistry: " + e.getMessage()); + } + + try { + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + if (problemHolder != null) { + problemHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing cache: " + e.getMessage()); + } + + try { + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + if (stateHolder != null) { + stateHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ State holder cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing state: " + e.getMessage()); + } + + initializedProjects.remove(project.getName()); + CxLogger.info(LOG_TAG + " ✓ Project cleanup completed: " + project.getName()); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error cleaning up project " + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isInitialized(IProject project) { + return initializedProjects.contains(project.getName()); + } + + public String getStatistics() { + return "Initialized projects: " + initializedProjects.size(); + } + + private void startWorkspaceFileScanning(IProject project) { + Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Scanning manifest, IaC, and container files...", 3); + + // Check if job was cancelled or project closed before starting + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanManifestFiles(project); + monitor.worked(1); + + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanIacFiles(project); + monitor.worked(1); + + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanContainerFiles(project); + monitor.worked(1); + + return Status.OK_STATUS; + + } catch (Exception e) { + e.printStackTrace(); + return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); + } finally { + monitor.done(); + } + } + }; + + try { + // Store job reference in session property so onProjectClose() can cancel it + project.setSessionProperty(WORKSPACE_SCAN_JOB_KEY, scanJob); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error storing workspace scan job: " + e.getMessage()); + } + + // Run as a background job so it doesn't block the IDE + scanJob.setPriority(Job.BUILD); + scanJob.schedule(); + } + + private void scanManifestFiles(IProject project) { + String[] manifestPatterns = { + "pom.xml", "package.json", "package-lock.json", "npm-shrinkwrap.json", + "go.mod", "go.sum", "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", + "Gemfile", "Gemfile.lock", "Cargo.toml", "Cargo.lock", "composer.json", "composer.lock", + "packages.config", ".csproj", "yarn.lock" + }; + findAndScanFiles(project, manifestPatterns, "OSS Manifest Files"); + } + + private void scanIacFiles(IProject project) { + String[] iacPatterns = { ".tf", ".tfvars", ".yaml", ".yml", ".hcl" }; + findAndScanFiles(project, iacPatterns, "IaC Configuration Files"); + } + + private void scanContainerFiles(IProject project) { + String[] containerPatterns = { + "Dockerfile", "dockerfile", "docker-compose.yaml", "docker-compose.yml", ".dockerignore" + }; + findAndScanFiles(project, containerPatterns, "Container Files"); + } + + private void findAndScanFiles(IProject project, String[] patterns, String fileType) { + try { + + + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "scanner-registry")); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "state-holder")); + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "problem-holder")); + + if (registry == null || stateHolder == null || problemHolder == null) { + return; + } + + IResource[] members = project.members(true); + for (IResource resource : members) { + if (!(resource instanceof org.eclipse.core.resources.IFile)) { + continue; + } + + IFile file = (org.eclipse.core.resources.IFile) resource; + String fileName = file.getName().toLowerCase(); + String filePath = file.getLocation().toOSString(); + + boolean matches = false; + for (String pattern : patterns) { + if (fileName.equals(pattern.toLowerCase()) || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { + matches = true; + break; + } + } + if (matches) { + try { + ScanManager scanManager = new ScanManager(registry, stateHolder); + List issues = scanManager.scanFile(filePath); + if (!issues.isEmpty()) { + problemHolder.addScanIssues(filePath, issues); + ResultPublisher.publishResults(file, issues); + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error scanning " + fileName + ": " + e.getMessage()); + } + } + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error finding files for " + fileType + ": " + e.getMessage()); + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java new file mode 100644 index 00000000..ae812f57 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -0,0 +1,294 @@ +package com.checkmarx.eclipse.devassist.backend.result; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.inspection.DevAssistInspectionMgr; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.List; + +/** + * Publishes scan results to Checkmarx Findings Window and editor decorations. + * + * Responsibilities: + * - Update custom Findings View with scan results + * - Render editor decorations (gutter icons, underlines) for Findings Window issues + * - NO integration with Eclipse native Problems View + * + * This connects scan results directly to the custom Findings Window. + */ +public class ResultPublisher { + + private static final String LOG_TAG = "[RESULT-PUBLISHER]"; + + /** + * Publish scan results to Findings View and editor decorations. + * + * Orchestrates the complete problem descriptor creation and publication flow: + * 1. Update Findings View cache with scan results + * 2. Create problem descriptors via DevAssistInspectionMgr + * 3. Render editor decorations (gutter icons, underlines) + * + * Mirrors JetBrains pattern where scan results are stored in cache, + * which then publishes a message to notify all interested views. + * + * @param file File that was scanned + * @param scanIssues Issues found by scanners + */ + public static void publishResults(IFile file, List scanIssues) { + if (file == null || scanIssues == null) { + return; + } + try { + // Step 1: Update Findings View (try to display immediately if view is open) + + updateFindingsView(file, scanIssues); + + + // Step 2: Create problem descriptors via DevAssistInspectionMgr + + createAndRenderDecorations(file, scanIssues); + + + } catch (Exception e) { + System.err.println(LOG_TAG + " [ERROR] " + e.getMessage()); + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error publishing results: " + e.getMessage(), e); + } + } + + /** + * Update Findings View with scan results. + * + * @param file File that was scanned + * @param scanIssues Issues to display + */ + private static void updateFindingsView(IFile file, List scanIssues) { + try { + if (scanIssues.isEmpty()) { + return; + } + + // Must run on UI thread + org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + + // JetBrains Pattern: Remove old engine results, then merge new results + // This triggers the message bus pattern: + // 1. removeScanIssuesByFileAndScanner() removes old results for THIS engine + // 2. mergeScanIssues() stores new results in cache + // 3. notifyListenersOfUpdate() publishes to all listeners + // 4. CxFindingsView listener receives callback with getAllIssues() + // 5. Listener calls refreshTree(allCachedResults) + // 6. Tree shows merged results (no duplicates, no stale issues) + // FIX: Use getLocation() (absolute path) to match cache key format used in RealTimeScanJob + // ProblemHolderService cache is keyed with absolute paths from RealTimeScanJob.scanFile() + // Must use same path format for cache lookups or removal will fail - causing duplicates + String filePath = file.getLocation().toOSString(); + + org.eclipse.core.resources.IProject project = file.getProject(); + if (project != null) { + ProblemHolderService problemHolder = + (ProblemHolderService) project.getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + // Get engine type from scan issues (all issues from same scan have same engine) + String engineType = scanIssues.isEmpty() ? null : + scanIssues.get(0).getScanEngine() != null ? + scanIssues.get(0).getScanEngine().name() : null; + + // Step 1: Remove old results from THIS scanner engine + if (engineType != null) { + problemHolder.removeScanIssuesByFileAndScanner(engineType, filePath); + + } + + // Step 2: Add new results from THIS scanner engine + problemHolder.mergeScanIssues(filePath, scanIssues); + + } else { + + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Create problem descriptors and render editor decorations. + * + * Orchestrates: + * 1. Get registry and state holder from project session + * 2. Build ProblemHelper.Builder with file context and scan issues + * 3. Call DevAssistInspectionMgr to create problem descriptors + * 4. Render gutter icons and underlines using descriptors + * + * @param file File that was scanned + * @param scanIssues Issues to process + */ + private static void createAndRenderDecorations(IFile file, List scanIssues) { + try { + if (scanIssues.isEmpty()) { + return; + } + + org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + + org.eclipse.core.resources.IProject project = file.getProject(); + if (project == null) { + CxLogger.warning(LOG_TAG + " Project not available for file: " + file.getName()); + return; + } + + display.asyncExec(() -> { + try { + // Get registry and state holder from session properties + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "scanner-registry")); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "state-holder")); + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (registry == null || stateHolder == null || problemHolder == null) { + CxLogger.warning(LOG_TAG + " Required services not initialized (registry=" + (registry != null) + + ", stateHolder=" + (stateHolder != null) + ", problemHolder=" + (problemHolder != null) + ")"); + // Fallback to direct decoration if services not available + ProblemDecorator.decorateEditor(file, scanIssues); + return; + } + + // Build ProblemHelper.Builder with file context and scan issues + String filePath = file.getLocation().toOSString(); + org.eclipse.jface.text.IDocument document = getDocumentForFile(file); + ProblemHelper.Builder builder = ProblemHelper.builder(file, project) + .filePath(filePath) + .document(document) + .scanIssueList(scanIssues) + .problemHolderService(problemHolder) + .problemDecorator(new ProblemDecorator()); + + // Create problem descriptors via DevAssistInspectionMgr + DevAssistInspectionMgr mgr = new DevAssistInspectionMgr(registry, stateHolder); + mgr.startScanAndCreateProblemDescriptors(builder); + + CxLogger.info(LOG_TAG + " Problem descriptors created via DevAssistInspectionMgr for " + scanIssues.size() + " issues"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating problem descriptors: " + e.getMessage()); + // Fallback to direct decoration + try { + ProblemDecorator.decorateEditor(file, scanIssues); + } catch (Exception fallbackError) { + CxLogger.error(LOG_TAG + " Fallback decoration also failed: " + fallbackError.getMessage(), fallbackError); + } + } + }); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error: " + e.getMessage()); + } + } + + /** + * Get the IDocument for a file, preferring the live editor's document (so unsaved + * edits are reflected) and falling back to reading the file's on-disk content. + * + * ScanIssueProcessor requires a non-null document to validate that an issue's line + * number is within range (getNumberOfLines()); without it every issue is rejected. + * + * @param file File to get the document for + * @return IDocument, or null if it could not be obtained + */ + private static org.eclipse.jface.text.IDocument getDocumentForFile(IFile file) { + try { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page != null) { + org.eclipse.ui.IEditorPart editor = page.findEditor(new org.eclipse.ui.part.FileEditorInput(file)); + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + if (doc != null) { + return doc; + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Could not get document from editor: " + e.getMessage()); + } + + try { + org.eclipse.jface.text.Document doc = new org.eclipse.jface.text.Document(); + doc.set(new String(file.getContents().readAllBytes())); + return doc; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Could not create document from file: " + e.getMessage()); + return null; + } + } + + /** + * Find the open Findings View. + * + * @return CxFindingsView instance if open, null otherwise + */ + private static com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView findOpenFindingsView() { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + + IWorkbenchPage page = null; + try { + page = workbench.getActiveWorkbenchWindow().getActivePage(); + } catch (NullPointerException e) { + for (var window : workbench.getWorkbenchWindows()) { + page = window.getActivePage(); + if (page != null) break; + } + } + + if (page == null) { + return null; + } + + return (com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView) page + .findView(com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView.ID); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error finding Findings View: " + e.getMessage()); + return null; + } + } + + /** + * Clear results for a file. + * + * @param file File to clear + */ + public static void clearResults(IFile file) { + try { + CxLogger.info(LOG_TAG + " Clearing results for: " + file.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing results: " + e.getMessage()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java new file mode 100644 index 00000000..bf747a40 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java @@ -0,0 +1,114 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; + +/** + * BaseScannerCommand is an abstract implementation of the ScannerCommand interface + * that provides foundational functionality for registering, deregistering, and + * managing a scanner's lifecycle for a given project. This class serves as a + * base implementation for custom scanner commands. + */ +public abstract class BaseScannerCommand implements ScannerCommand { + + private static final String LOG_TAG = "[SCANNER-COMMAND]"; + public ScannerConfig config; + protected IProject project; + private boolean isRegistered = false; + + /** + * Create a scanner command with configuration. + * + * @param project Eclipse project + * @param config Scanner configuration + */ + protected BaseScannerCommand(IProject project, ScannerConfig config) { + this.project = project; + this.config = config; + } + + /** + * Registers the project for the scanner which is invoked + * + * @param project - the project for the registration + */ + @Override + public void register(IProject project) { + boolean isActive = getScannerActivationStatus(); + if (!isActive) { + return; + } + if (isScannerRegisteredAlready(project)) { + return; + } + CxLogger.info(config.getEnabledMessage() + ":" + project.getName()); + initializeScanner(); + isRegistered = true; + } + + /** + * De-registers the project for the scanner. + * This method is called in two cases: either project is closed by the user, or scanner is disabled + * + * @param project - the project that is registered + */ + @Override + public void deregister(IProject project) { + if (!isScannerRegisteredAlready(project)) { + return; + } + CxLogger.info(config.getDisabledMessage() + ":" + project.getName()); + isRegistered = false; + } + + /** + * Returns the scanner activation status of the scanner engine + */ + private boolean getScannerActivationStatus() { + return config != null && config.getEngineName() != null; + } + + /** + * Checks if the scanner is registered already for the project + * + * @param project is required + */ + private boolean isScannerRegisteredAlready(IProject project) { + return isRegistered; + } + + /** + * This method returns the ScanEngine Type + * + * @return ScanEngine + */ + protected ScanEngine getScannerType() { + return ScanEngine.valueOf(config.getEngineName().toUpperCase()); + } + + /** + * Get the configuration. + * + * @return Scanner config + */ + public ScannerConfig getConfig() { + return config; + } + + /** + * Abstract method to initialize the scanner + * This method is invoked when the scanner is registered for the project + */ + @Override + public abstract void initializeScanner(); + + /** + * Dispose the scanner. + */ + @Override + public void dispose() { + CxLogger.info(LOG_TAG + " Disposed"); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java new file mode 100644 index 00000000..a1cd195a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java @@ -0,0 +1,144 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.stream.Stream; + +/** + * Base implementation of {@link ScannerService} that wires respective ScannerConfig called + * from different scannerServices. + * Provides helpers for deciding when to scan files and scanners managing temporary folders. + * + * @param is type of ScanResult produced by concrete scanner Scan method implementations + */ +public abstract class BaseScannerService implements ScannerService { + + protected final IProject project; + public ScannerConfig config; + private static final String LOG_TAG = "[SCANNER-SERVICE]"; + + /** + * Creates a new scanner service with the supplied configuration. + * + * @param project Eclipse project + * @param config configuration values to be used by the scanner + */ + public BaseScannerService(IProject project, ScannerConfig config) { + this.project = project; + this.config = config; + } + + /** + * Determines whether the file at the given path should be scanned. + * Files inside /node_modules/ are skipped by default. + * + * @param filePath absolute or project-relative file path + * @return true if the file should be scanned; false otherwise + */ + @Override + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + + // Common exclusions + if (filePath.contains("/node_modules/") || filePath.contains("\\node_modules\\")) { + return false; + } + + return isFileTypeSupported(filePath); + } + + /** + * Subclasses implement scanner-specific file type checking. + * + * @param filePath File path + * @return true if scanner supports this file + */ + protected abstract boolean isFileTypeSupported(String filePath); + + /** + * Perform scan - subclasses must implement this. + * + * @param filePath File to scan + * @return ScanResult of type T or null + */ + @Override + public abstract ScanResult scan(String filePath); + + /** + * Get the configuration. + * + * @return Scanner config + */ + @Override + public ScannerConfig getConfig() { + return config; + } + + /** + * Builds the path to a temporary sub-folder within the system temp directory. + * + * @param baseDir name of the sub-folder to create under java.io.tmpdir + * @return absolute path string for the temporary sub-folder + */ + protected String getTempSubFolderPath(String baseDir) { + String tempOS = System.getProperty("java.io.tmpdir"); + Path tempDir = Paths.get(tempOS, baseDir); + return tempDir.toString(); + } + + /** + * Ensures that the specified temporary folder exists, creating any missing directories. + * + * @param folderPath target temporary folder path + */ + protected void createTempFolder(Path folderPath) { + try { + Files.createDirectories(folderPath); + } catch (IOException e) { + CxLogger.warning("Failed to create temporary folder:" + folderPath); + } + } + + /** + * Recursively deletes the provided temporary folder and files in it, if it has been created. + * + * @param tempFolder root path of the temporary folder to remove + */ + protected void deleteTempFolder(Path tempFolder) { + if (Files.notExists(tempFolder)) { + return; + } + try (Stream walk = Files.walk(tempFolder)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception e) { + CxLogger.warning("Failed to delete file in temp folder:" + path); + } + }); + } catch (IOException e) { + CxLogger.warning("Failed to delete temporary folder:" + tempFolder); + } + } + + /** + * Close the scanner and release resources. + * + * @throws Exception if close fails + */ + @Override + public void close() throws Exception { + CxLogger.info(LOG_TAG + " Closed for project: " + project.getName()); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java new file mode 100644 index 00000000..27057f33 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java @@ -0,0 +1,34 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import org.eclipse.core.resources.IProject; + +/** + * Interface for scanner command implementations. + * Manages scanner lifecycle including registration and deregistration. + */ +public interface ScannerCommand { + + /** + * Register the scanner for a project. + * + * @param project Eclipse project + */ + void register(IProject project); + + /** + * Deregister the scanner for a project. + * + * @param project Eclipse project + */ + void deregister(IProject project); + + /** + * Initialize the scanner. + */ + void initializeScanner(); + + /** + * Dispose the scanner. + */ + void dispose(); +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java new file mode 100644 index 00000000..c1121b7f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java @@ -0,0 +1,43 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; + +/** + * Generic interface for scanner services. + * Each scanner produces a specific result type T. + * + * @param The result type produced by this scanner + */ +public interface ScannerService { + + /** + * Check if this scanner should scan the file. + * + * @param filePath File path + * @return true if file should be scanned + */ + boolean shouldScanFile(String filePath); + + /** + * Perform a scan on the file and return result. + * + * @param filePath File path + * @return ScanResult of type T or null + */ + ScanResult scan(String filePath); + + /** + * Get the scanner configuration. + * + * @return Scanner config + */ + ScannerConfig getConfig(); + + /** + * Close scanner and release resources. + * + * @throws Exception if close fails + */ + void close() throws Exception; +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java new file mode 100644 index 00000000..8c01134f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -0,0 +1,185 @@ +package com.checkmarx.eclipse.devassist.common; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Orchestrates the scanning process for a file. + * + * Responsibilities: - Use ScannerFactory to select appropriate scanners for a + * file - Check if file has changed since last scan (skip redundant scans) - + * Execute all applicable scanners in sequence - Merge results from multiple + * scanners - Update file state timestamp to prevent re-scanning + * + * This is the main entry point for initiating scans. Called from + * FileEditorListener when a file is modified. + * + * NOTE: Uses backend.DevAssistScanStateHolder (not inspection.version) to maintain compatibility + * with existing code that passes backend version to super(). + */ +public class ScanManager { + + private static final String LOG_TAG = "[SCAN-MANAGER]"; + + private final ScannerFactory factory; + private final com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder; + + /** + * Create a scan manager for a project. + * + * @param registry Project's scanner registry + * @param stateHolder State holder for tracking file modification + */ + public ScanManager(ScannerRegistry registry, DevAssistScanStateHolder stateHolder) { + this.factory = new ScannerFactory(registry); + this.stateHolder = stateHolder; + } + + /** + * Scan a file using all applicable scanners. + * + * High-level flow: 1. Compute current file state hash 2. Check if file changed + * since last scan 3. If unchanged, return cached results 4. Get all scanners + * that support this file 5. Execute each scanner sequentially 6. Merge results + * from all scanners 7. Update state hash to mark as scanned 8. Return merged + * results + * + * @param filePath Absolute file path to scan + * @return List of issues found by all scanners + * @throws Exception if scan fails + */ + public List scanFile(String filePath) throws Exception { + if (filePath == null || filePath.isEmpty()) { + + return List.of(); + } + + + + + + + // 1. Compute current file state hash + + long currentStateHash = DevAssistScanStateHolder.computeFileStateHash(filePath); + + + // 2. Check if file changed since last scan + // NOTE: hasChanged() atomically marks the file as "in-flight" when it returns true. + // We MUST call stateHolder.markScanComplete(filePath) once we're done (success or + // failure) or every subsequent edit will be permanently BLOCKED as "already in-flight". + if (!stateHolder.hasChanged(filePath, currentStateHash)) { + + return List.of(); + } + + try { + // 3. Get all scanners that support this file + + List> applicableScanners = factory.getAllSupportedScanners(filePath); + + + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; + + } + + if (applicableScanners.isEmpty()) { + + // Still update state to avoid re-checking unsupported files + stateHolder.updateStateHash(filePath, currentStateHash); + return List.of(); + } + + // 4. Execute all scanners and merge results + + List allIssues = new ArrayList<>(); + int scannerIndex = 1; + int successfulScanners = 0; + + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; + try { + var scanResult = scanner.scan(filePath); + List scannerResults = scanResult != null ? scanResult.getIssues() : null; + + if (scannerResults != null) { + for (ScanIssue issue : scannerResults) { + } + allIssues.addAll(scannerResults); + } + successfulScanners++; + + } catch (Exception e) { + e.printStackTrace(); + } + scannerIndex++; + } + + // 5. Update state hash only if at least one scanner succeeded + // If all scanners failed, don't update hash so file will be re-scanned on next change + if (successfulScanners > 0) { + stateHolder.updateStateHash(filePath, currentStateHash); + } + + return allIssues; + } finally { + // Always release the in-flight marker so the next edit can trigger a scan. + stateHolder.markScanComplete(filePath); + } + } + + /** + * Scan a file using a specific scanner type. + * + * Used when you want to force a scan with a particular scanner, regardless of + * file type. + * + * @param filePath File to scan + * @param scannerType Specific scanner to use + * @return Issues from that scanner, or empty list if scanner doesn't support + * file + * @throws Exception if scan fails + */ + public List scanFileWithScanner(String filePath, ScannerType scannerType) throws Exception { + + if (filePath == null || scannerType == null) { + return List.of(); + } + + CxLogger.info(LOG_TAG + " Starting " + scannerType.getDisplayName() + " scan: " + filePath); + + ScannerService scanner = factory.getScannerForFile(filePath, scannerType); + if (scanner == null) { + CxLogger.warning(LOG_TAG + " Scanner does not support file: " + filePath); + return List.of(); + } + + try { + var scanResult = scanner.scan(filePath); + List results = scanResult != null ? scanResult.getIssues() : List.of(); + CxLogger.info(LOG_TAG + " Found " + results.size() + " issues"); + return results; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Scan failed: " + e.getMessage(), e); + throw e; + } + } + + /** + * Get factory statistics. + * + * @return Summary string + */ + public String getStatistics() { + return factory.getStatistics(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java new file mode 100644 index 00000000..1b7f240f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java @@ -0,0 +1,32 @@ +package com.checkmarx.eclipse.devassist.common; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import java.util.List; + +/** + * Interface for a scan result wrapper. + * + * Adaptor classes implement this interface to wrap raw scanner results + * and provide conversion to standardized ScanIssue objects. + * + * @param Type of raw scanner result (e.g., OssRealtimeResults, SecretsRealtimeResults) + */ +public interface ScanResult { + + /** + * Get the raw scan results from the scanner. + * + * @return Raw scanner results of type T + */ + T getResults(); + + /** + * Get the standardized list of scan issues from the raw results. + * + * This converts the scanner-specific result format into a uniform + * list of ScanIssue objects that can be displayed in the UI. + * + * @return List of ScanIssue objects + */ + List getIssues(); +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java new file mode 100644 index 00000000..11a88458 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java @@ -0,0 +1,95 @@ +package com.checkmarx.eclipse.devassist.common; + +/** + * Configuration object for scanner engines. + * Defines settings and messages for each scanner type. + */ +public class ScannerConfig { + + private final String engineName; + private final String configSection; + private final String activateKey; + private final String enabledMessage; + private final String disabledMessage; + private final String errorMessage; + + private ScannerConfig(Builder builder) { + this.engineName = builder.engineName; + this.configSection = builder.configSection; + this.activateKey = builder.activateKey; + this.enabledMessage = builder.enabledMessage; + this.disabledMessage = builder.disabledMessage; + this.errorMessage = builder.errorMessage; + } + + public static Builder builder() { + return new Builder(); + } + + public String getEngineName() { + return engineName; + } + + public String getConfigSection() { + return configSection; + } + + public String getActivateKey() { + return activateKey; + } + + public String getEnabledMessage() { + return enabledMessage; + } + + public String getDisabledMessage() { + return disabledMessage; + } + + public String getErrorMessage() { + return errorMessage; + } + + public static class Builder { + private String engineName; + private String configSection; + private String activateKey; + private String enabledMessage; + private String disabledMessage; + private String errorMessage; + + public Builder engineName(String engineName) { + this.engineName = engineName; + return this; + } + + public Builder configSection(String configSection) { + this.configSection = configSection; + return this; + } + + public Builder activateKey(String activateKey) { + this.activateKey = activateKey; + return this; + } + + public Builder enabledMessage(String enabledMessage) { + this.enabledMessage = enabledMessage; + return this; + } + + public Builder disabledMessage(String disabledMessage) { + this.disabledMessage = disabledMessage; + return this; + } + + public Builder errorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + public ScannerConfig build() { + return new ScannerConfig(this); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java new file mode 100644 index 00000000..cd0e3e70 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java @@ -0,0 +1,188 @@ +package com.checkmarx.eclipse.devassist.common; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Factory for selecting appropriate scanners by file type. + * + * Responsibilities: + * - Query all available scanners + * - Filter by file type compatibility + * - Filter by global enabled state + * - Return ordered list of applicable scanners + * + * Mirrors the JetBrains ScannerFactory pattern. + */ +public class ScannerFactory { + + private static final String LOG_TAG = "[SCANNER-FACTORY]"; + + private final ScannerRegistry registry; + private final GlobalScannerController controller; + + /** + * Create a scanner factory for a project. + * + * @param registry Project's scanner registry + */ + public ScannerFactory(ScannerRegistry registry) { + this.registry = registry; + this.controller = GlobalScannerController.getInstance(); + } + + /** + * Get all enabled scanners that support a file. + * + * Queries all scanner types, filters by: + * 1. Global enabled state (GlobalScannerController) + * 2. File type support (ScannerService.shouldScanFile()) + * + * @param filePath File to scan + * @return List of applicable scanners (empty if none match) + */ + public List> getAllSupportedScanners(String filePath) { + List> supported = new ArrayList<>(); + + CxLogger.info(LOG_TAG + " Finding scanners for: " + filePath); + + // Check each scanner type + for (ScannerType type : ScannerType.values()) { + // Check if globally enabled + if (!controller.isScannerEnabled(type)) { + CxLogger.info(LOG_TAG + " ⊘ " + type.getDisplayName() + " disabled globally"); + continue; + } + + // Get scanner from registry + ScannerService scanner = getScannerService(type); + if (scanner == null) { + CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); + continue; + } + + // Check if supports this file type + if (scanner.shouldScanFile(filePath)) { + supported.add(scanner); + CxLogger.info(LOG_TAG + " ✓ " + type.getDisplayName() + " supports file"); + } else { + CxLogger.info(LOG_TAG + " ⊘ " + type.getDisplayName() + " does not support file"); + } + } + + if (supported.isEmpty()) { + CxLogger.info(LOG_TAG + " No scanners support this file"); + } else { + CxLogger.info(LOG_TAG + " ✓ Found " + supported.size() + " supporting scanner(s)"); + } + + return supported; + } + + /** + * Get a specific scanner by type if it supports the file. + * + * @param filePath File to scan + * @param type Scanner type to retrieve + * @return Scanner if enabled and supports file, null otherwise + */ + public ScannerService getScannerForFile(String filePath, ScannerType type) { + if (filePath == null || type == null) { + return null; + } + + // Check if globally enabled + if (!controller.isScannerEnabled(type)) { + CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " is disabled globally"); + return null; + } + + // Get scanner from registry + ScannerService scanner = getScannerService(type); + if (scanner == null) { + CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); + return null; + } + + // Check if supports file type + if (!scanner.shouldScanFile(filePath)) { + CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " does not support file: " + + filePath); + return null; + } + + return scanner; + } + + /** + * Get a scanner service by type. + * Retrieves from the registry which manages scanner lifecycle. + * + * @param type Scanner type + * @return Scanner instance, or null if not available + */ + private ScannerService getScannerService(ScannerType type) { + try { + Object scanner = registry.getScannerService(type); + return scanner instanceof ScannerService ? (ScannerService) scanner : null; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error getting scanner for type " + type + ": " + e.getMessage()); + return null; + } + } + + /** + * Get scanner by file name pattern (useful for quick lookups). + * Returns the primary scanner for a file type. + * + * @param filePath File path + * @return Primary scanner type for this file, or null + */ + public ScannerType getPrimaryScannerType(String filePath) { + if (filePath == null) { + return null; + } + + String lowerPath = filePath.toLowerCase(); + + // Manifest files → OSS + if (lowerPath.matches(".*\\.(package\\.json|pom\\.xml|go\\.mod|requirements\\.txt|" + + "Gemfile|Cargo\\.toml|Pipfile)$")) { + return ScannerType.OSS; + } + + // Source code files → ASCA + if (lowerPath.matches(".*\\.(java|py|js|ts|cpp|cs|go|php|rb|swift)$")) { + return ScannerType.ASCA; + } + + // Infrastructure files → IAC + if (lowerPath.matches(".*\\.(tf|yaml|yml|json|hcl)$")) { + return ScannerType.IAC; + } + + // Container files → CONTAINERS + if (lowerPath.matches(".*(Dockerfile|docker-compose\\.ya?ml)")) { + return ScannerType.CONTAINERS; + } + + // Everything else can be scanned for secrets + return ScannerType.SECRETS; + } + + /** + * Get factory statistics. + * + * @return Summary string + */ + public String getStatistics() { + int enabledCount = controller.getEnabledScannerCount(); + return "Scanners enabled: " + enabledCount + "/" + ScannerType.values().length; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java new file mode 100644 index 00000000..d5c1f8f2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java @@ -0,0 +1,49 @@ +package com.checkmarx.eclipse.devassist.factory; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.properties.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Builds wrapper objects according to the current configuration. + */ +public class CxWrapperFactory { + + public static CxWrapper build() throws CxException, Exception { + return getWrapper(); + } + + /** + * Create a CxWrapper with current credentials and configuration + * + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + private static CxWrapper getWrapper() throws Exception { + CxWrapper cxWrapper = null; + + Logger log = LoggerFactory.getLogger(CxWrapperFactory.class.getName()); + + CxConfig.CxConfigBuilder builder = CxConfig.builder() + .apiKey(Preferences.getApiKey()) + .additionalParameters(Preferences.getAdditionalOptions()); + + CxConfig config = builder.build(); + + try { + cxWrapper = new CxWrapper(config, log); + } catch (IOException e) { + CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); + throw new Exception(e); + } + + return cxWrapper; + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java new file mode 100644 index 00000000..8dbd41d1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java @@ -0,0 +1,45 @@ +package com.checkmarx.eclipse.devassist.inspection; + +/** + * Inspection metadata and registry class. + * + * In JetBrains: extends LocalInspectionTool with checkFile() implementation. + * In Eclipse: serves as metadata holder for inspection framework integration. + * + * Provides inspection ID, name, and grouping constants for registration. + * Can be extended with inspection framework hooks in future. + */ +public class DevAssistInspection { + + // Inspection identity constants + private static final String INSPECTION_ID = "com.checkmarx.eclipse.devassist.inspection"; + private static final String INSPECTION_NAME = "Checkmarx Developer Assist"; + private static final String INSPECTION_GROUP = "Checkmarx"; + + /** + * Get the unique identifier for this inspection. + * + * @return Inspection ID for registration and lookup + */ + public String getInspectionId() { + return INSPECTION_ID; + } + + /** + * Get the human-readable name for this inspection. + * + * @return Inspection name for display in UI + */ + public String getInspectionName() { + return INSPECTION_NAME; + } + + /** + * Get the inspection group/category. + * + * @return Inspection group for organization in preferences + */ + public String getInspectionGroup() { + return INSPECTION_GROUP; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java new file mode 100644 index 00000000..d09fdd74 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java @@ -0,0 +1,334 @@ +package com.checkmarx.eclipse.devassist.inspection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.common.ScanManager; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemBuilder; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +import com.checkmarx.eclipse.devassist.problems.ProblemDescriptor; +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.problems.ScanIssueProcessor; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Main orchestrator for inspection workflow. + * + * Coordinates the complete flow: + * 1. Scan files using ScanManager (inherited) + * 2. Create problem descriptors from scan issues + * 3. Validate issues (ScanIssueProcessor) + * 4. Cache problems (ProblemHolderService) + * 5. Decorate editor (ProblemDecorator) + * 6. Manage cleanup and state reset + * + * Extends ScanManager to inherit scanning capabilities. + * Mirrors JetBrains DevAssistInspectionMgr. + */ +public class DevAssistInspectionMgr extends ScanManager { + + private static final String LOG_TAG = "[INSPECTION-MGR]"; + + private final ProblemDecorator problemDecorator = new ProblemDecorator(); + + /** + * Constructor accepting scanner registry and state holder. + * + * @param registry Scanner registry for the project + * @param stateHolder State holder for tracking file modifications + */ + public DevAssistInspectionMgr( + ScannerRegistry registry, + com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder) { + super(registry, stateHolder); + } + + /** + * Scan a file and create problem descriptors. + * + * Complete orchestration: + * 1. Build problem helper + * 2. Scan file → get ScanIssue list (if not already provided) + * 3. Cache scan issues + * 4. Create ScanIssueProcessor for validation + * 5. For each issue: validate and create ProblemDescriptor + * 6. Cache problem descriptors + * 7. Return array of problem descriptors + * + * @param problemHelperBuilder Builder with pre-configured context + * @return Array of problem descriptors (empty if none) + */ + public ProblemDescriptor[] startScanAndCreateProblemDescriptors( + ProblemHelper.Builder problemHelperBuilder) { + + ProblemHelper problemHelper = problemHelperBuilder.build(); + + CxLogger.info(LOG_TAG + " Starting scan for file: " + problemHelper.getFile().getName()); + + try { + // Use pre-scanned issues if available, otherwise scan file + List allScanIssues = problemHelper.getScanIssueList(); + if (allScanIssues == null || allScanIssues.isEmpty()) { + allScanIssues = scanFile(problemHelper.getFilePath()); + CxLogger.info(LOG_TAG + " Performed fresh scan for file: " + problemHelper.getFile().getName()); + } else { + CxLogger.info(LOG_TAG + " Using pre-scanned issues for file: " + problemHelper.getFile().getName()); + } + + if (allScanIssues.isEmpty()) { + CxLogger.info(LOG_TAG + " No scan issues found for: " + + problemHelper.getFile().getName()); + decorateUIForIgnoreVulnerability(problemHelper.getFile(), allScanIssues); + return new ProblemDescriptor[0]; + } + + // Ensure helper has the issues (in case they were pre-populated) + problemHelperBuilder.scanIssueList(allScanIssues); + ProblemHelper helperWithIssues = problemHelperBuilder.build(); + + // Cache issues + helperWithIssues.getProblemHolderService().addScanIssues( + problemHelper.getFilePath(), allScanIssues); + + // Create problems with decoration + List allProblems = createProblemDescriptorsWithDecoration(helperWithIssues); + + if (allProblems.isEmpty()) { + CxLogger.info(LOG_TAG + " No problem descriptors created for: " + + problemHelper.getFile().getName()); + return new ProblemDescriptor[0]; + } + + // Cache problem descriptors + helperWithIssues.getProblemHolderService().addProblemDescriptors( + problemHelper.getFilePath(), allProblems); + + CxLogger.info(LOG_TAG + " Created " + allProblems.size() + + " problem descriptors for: " + problemHelper.getFile().getName()); + + return allProblems.toArray(new ProblemDescriptor[0]); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error during scan: " + e.getMessage(), e); + return new ProblemDescriptor[0]; + } + } + + /** + * Create problem descriptors with UI decoration. + * + * Removes existing annotations, validates issues, creates descriptors, + * and decorates the editor with visual feedback. + * + * @param problemHelper Helper with scan issues + * @return List of created problem descriptors + */ + private List createProblemDescriptorsWithDecoration( + ProblemHelper problemHelper) { + + if (isScanIssuePresent(problemHelper.getScanIssueList())) { + // Clear existing decorations + ProblemDecorator.removeAllHighlighters(problemHelper.getProject()); + + // Process issues with decoration enabled + List descriptors = createProblemDescriptors( + problemHelper, true); + + // Decorate UI + if (!descriptors.isEmpty()) { + decorateUI(problemHelper.getDocument(), problemHelper.getFile(), + problemHelper.getScanIssueList()); + } + + return descriptors; + } + return Collections.emptyList(); + } + + /** + * Create problem descriptors without UI decoration. + * + * @param problemHelper Helper with scan issues + * @return List of created problem descriptors + */ + public List createProblemDescriptorsWithoutDecoration( + ProblemHelper problemHelper) { + + if (isScanIssuePresent(problemHelper.getScanIssueList())) { + return createProblemDescriptors(problemHelper, false); + } + return Collections.emptyList(); + } + + /** + * Create problem descriptors from scan issues. + * + * For each scan issue: + * 1. Create ScanIssueProcessor + * 2. Validate and create ProblemDescriptor + * 3. Collect non-null descriptors + * + * @param problemHelper Helper with context and issues + * @param isDecoratorEnabled Whether to enable visual decoration + * @return List of valid problem descriptors + */ + private List createProblemDescriptors( + ProblemHelper problemHelper, + boolean isDecoratorEnabled) { + + List descriptors = new ArrayList<>(); + ScanIssueProcessor processor = new ScanIssueProcessor(problemHelper); + + for (ScanIssue scanIssue : problemHelper.getScanIssueList()) { + ProblemDescriptor descriptor = processor.processScanIssue( + scanIssue, isDecoratorEnabled); + if (descriptor != null) { + descriptors.add(descriptor); + } + } + + CxLogger.info(LOG_TAG + " Created " + descriptors.size() + + " problem descriptors from " + problemHelper.getScanIssueList().size() + + " scan issues"); + + return descriptors; + } + + /** + * Get existing problem descriptors for a file. + * + * Called when file hasn't changed since last scan. + * Returns cached problem descriptors. + * + * @param problemHolderService Cache service + * @param filePath File path + * @param document Document (for validation) + * @param file IFile + * @param supportedEnabledScanners Enabled scanners + * @return Array of cached problem descriptors + */ + public ProblemDescriptor[] getExistingProblems( + ProblemHolderService problemHolderService, + String filePath, + IDocument document, + IFile file, + List supportedEnabledScanners) { + + ProblemHelper problemHelper = ProblemHelper.builder(file, file.getProject()) + .filePath(filePath) + .document(document) + .supportedScanners(supportedEnabledScanners) + .problemHolderService(problemHolderService) + .problemDecorator(this.problemDecorator) + .build(); + + // Get cached issues + List scanIssueList = problemHolderService.getScanIssuesByFile(filePath); + if (scanIssueList.isEmpty()) { + CxLogger.warning(LOG_TAG + " No cached issues for: " + filePath); + resetEditorAndResults(file.getProject(), filePath); + decorateUIForIgnoreVulnerability(file, scanIssueList); + return new ProblemDescriptor[0]; + } + + // Get cached problem descriptors + List cachedDescriptors = problemHolderService.getProblemDescriptors(filePath); + if (cachedDescriptors.isEmpty()) { + CxLogger.warning(LOG_TAG + " No cached problem descriptors for: " + filePath); + decorateUIForIgnoreVulnerability(file, scanIssueList); + return new ProblemDescriptor[0]; + } + + // Decorate UI with cached issues + decorateUI(document, file, scanIssueList); + + CxLogger.info(LOG_TAG + " Returning " + cachedDescriptors.size() + + " cached problem descriptors for: " + file.getName()); + + return cachedDescriptors.toArray(new ProblemDescriptor[0]); + } + + /** + * Decorate UI with scan results (gutter icons, underlines). + * + * @param document Document to decorate + * @param file File being decorated + * @param scanIssueList Issues to show + */ + public void decorateUI(IDocument document, IFile file, List scanIssueList) { + try { + ProblemDecorator.decorateEditor(file, scanIssueList); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error decorating UI: " + e.getMessage(), e); + } + } + + /** + * Decorate UI for ignored vulnerabilities (empty if none ignored). + * + * @param file File to decorate + * @param scanIssueList Issues (may be empty) + */ + public void decorateUIForIgnoreVulnerability(IFile file, List scanIssueList) { + try { + CxLogger.info(LOG_TAG + " decorateUIForIgnoreVulnerability called for: " + file.getName()); + // TODO: Integrate with IgnoredProblemsStore when available + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error in decorateUIForIgnoreVulnerability: " + e.getMessage(), e); + } + } + + /** + * Reset editor and clear all cached results for a file. + * + * Called when: + * - File is closed + * - Scan encounters error + * - User requests reset + * + * @param project Project containing file + * @param filePath File path to reset + */ + public void resetEditorAndResults(IProject project, String filePath) { + try { + if (project == null || !project.isOpen()) { + return; + } + + // Clear visual decorations + ProblemDecorator.removeAllHighlighters(project); + + // Clear cached data + ProblemHolderService problemHolderService = ProblemHolderService.getInstance(project); + if (problemHolderService != null && filePath != null && !filePath.isEmpty()) { + problemHolderService.removeProblemDescriptorsForFile(filePath); + problemHolderService.removeScanIssues(filePath); + } + + CxLogger.info(LOG_TAG + " Reset editor and results for: " + filePath); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error resetting: " + e.getMessage(), e); + } + } + + /** + * Check if scan issues are present. + * + * @param scanIssueList List to check + * @return true if not null and not empty + */ + private boolean isScanIssuePresent(List scanIssueList) { + return scanIssueList != null && !scanIssueList.isEmpty(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java new file mode 100644 index 00000000..1e4b3930 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java @@ -0,0 +1,188 @@ +package com.checkmarx.eclipse.devassist.inspection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; + +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Scheduler that wraps and coordinates RealTimeScanJob for background file scanning. + * + * Responsibilities: + * - Manage scheduling of real-time scans with debounce + * - Track pending scans per file + * - Cancel pending scans when needed + * - Provide clean API for scan orchestration + * + * Wraps Eclipse RealTimeScanJob which extends Job for background execution. + */ +public class DevAssistScanScheduler { + + private static final String LOG_TAG = "[SCAN-SCHEDULER]"; + private static final long DEFAULT_DEBOUNCE_DELAY_MS = 1000L; + + // Track pending jobs per file path + private final Map pendingScans = new ConcurrentHashMap<>(); + + /** + * Schedule a scan for a file with default debounce delay (1 second). + * + * If a scan is already pending for this file, returns false. + * Use reschedule() to cancel and restart with new delay. + * + * @param file File to scan + * @param problemHelper Problem context (unused in current impl, for alignment) + * @return true if scheduled, false if already pending + */ + public boolean scheduleInspection(IFile file, ProblemHelper problemHelper) { + return scheduleInspection(file, DEFAULT_DEBOUNCE_DELAY_MS); + } + + /** + * Schedule a scan for a file with custom debounce delay. + * + * @param file File to scan + * @param delayMs Debounce delay in milliseconds + * @return true if scheduled, false if already pending + */ + public boolean scheduleInspection(IFile file, long delayMs) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + + // Check if already pending + if (pendingScans.containsKey(filePath)) { + CxLogger.info(LOG_TAG + " Scan already pending for: " + filePath); + return false; + } + + try { + // Create new job + RealTimeScanJob scanJob = new RealTimeScanJob(file, file.getName()); + + // Track it + pendingScans.put(filePath, scanJob); + + // Schedule with debounce delay + scanJob.schedule(delayMs); + + CxLogger.info(LOG_TAG + " Scheduled scan for: " + filePath + + " (delay=" + delayMs + "ms)"); + return true; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to schedule scan: " + e.getMessage(), e); + pendingScans.remove(filePath); + return false; + } + } + + /** + * Reschedule a pending scan (cancel current, start new with delay). + * + * Used by CheckmarxDocumentListener when user types: + * - First keystroke: schedule with 1s delay + * - While typing: reschedule (cancel, start new 1s timer) + * - After user pauses: job runs + * + * @param file File to reschedule + * @param delayMs New debounce delay + * @return true if rescheduled, false if no pending job + */ + public boolean rescheduleInspection(IFile file, long delayMs) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + RealTimeScanJob existingJob = pendingScans.get(filePath); + + if (existingJob == null) { + // No pending job, schedule new one + return scheduleInspection(file, delayMs); + } + + try { + // Cancel current + existingJob.cancel(); + + // Reschedule with new delay + existingJob.reschedule(delayMs); + + CxLogger.info(LOG_TAG + " Rescheduled scan for: " + filePath + + " (delay=" + delayMs + "ms)"); + return true; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to reschedule: " + e.getMessage(), e); + return false; + } + } + + /** + * Cancel a pending scan for a file. + * + * @param file File to cancel scan for + * @return true if cancelled, false if no pending scan + */ + public boolean cancelScheduledInspection(IFile file) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + RealTimeScanJob job = pendingScans.remove(filePath); + + if (job == null) { + return false; + } + + try { + job.cancel(); + CxLogger.info(LOG_TAG + " Cancelled scan for: " + filePath); + return true; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error cancelling scan: " + e.getMessage(), e); + return false; + } + } + + /** + * Trigger inspection on the entire project (force re-inspection). + * + * @param project Project to inspect + */ + public void triggerInspection(IProject project) { + if (project == null) { + return; + } + CxLogger.info(LOG_TAG + " Triggering inspection for project: " + project.getName()); + // Future: force re-inspect all files in project + } + + /** + * Get number of pending scans. + * + * @return Count of scheduled but not yet running scans + */ + public int getPendingScansCount() { + return pendingScans.size(); + } + + /** + * Get statistics for debugging. + * + * @return Summary string + */ + public String getStatistics() { + return "Pending scans: " + pendingScans.size() + + ", Tracked files: " + pendingScans.keySet(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java new file mode 100644 index 00000000..7e010a9d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java @@ -0,0 +1,61 @@ +package com.checkmarx.eclipse.devassist.model; + +/** + * Represents a specific location within a file where a scan issue is detected. + * Contains line number and character range information. + */ +public class Location { + + private int line; + private int startIndex; + private int endIndex; + private boolean isAbsoluteOffset = false; + + public Location() { + } + + public Location(int line, int startIndex, int endIndex) { + this.line = line; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + public Location(int line, int startIndex, int endIndex, boolean isAbsoluteOffset) { + this.line = line; + this.startIndex = startIndex; + this.endIndex = endIndex; + this.isAbsoluteOffset = isAbsoluteOffset; + } + + public int getLine() { + return line; + } + + public void setLine(int line) { + this.line = line; + } + + public int getStartIndex() { + return startIndex; + } + + public void setStartIndex(int startIndex) { + this.startIndex = startIndex; + } + + public int getEndIndex() { + return endIndex; + } + + public void setEndIndex(int endIndex) { + this.endIndex = endIndex; + } + + public boolean isAbsoluteOffset() { + return isAbsoluteOffset; + } + + public void setAbsoluteOffset(boolean absoluteOffset) { + isAbsoluteOffset = absoluteOffset; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java new file mode 100644 index 00000000..6b7d6fbb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java @@ -0,0 +1,36 @@ +package com.checkmarx.eclipse.devassist.model; + +/** + * Enumeration of scan engines supported by Checkmarx. + */ +public enum ScanEngine { + ASCA("ASCA"), + OSS("OSS"), + SECRETS("SECRETS"), + CONTAINERS("CONTAINERS"), + IAC("IAC"); + + private final String displayName; + + ScanEngine(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } + + public static ScanEngine fromString(String value) { + for (ScanEngine engine : ScanEngine.values()) { + if (engine.displayName.equalsIgnoreCase(value)) { + return engine; + } + } + throw new IllegalArgumentException("Unknown scan engine: " + value); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java new file mode 100644 index 00000000..f72d961b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java @@ -0,0 +1,194 @@ +package com.checkmarx.eclipse.devassist.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a scan issue detected during a real-time scan. + * Captures detailed information about security issues identified in a scanned project. + * Each scan issue can have multiple locations and vulnerabilities. + */ +public class ScanIssue { + + private String scanIssueId; + private String severity; + private String title; + private String description; + private String remediationAdvise; + private String packageVersion; + private String packageManager; + private String cve; + private ScanEngine scanEngine; + private String filePath; + private String imageTag; + private String fileType; + private String secretValue; + private String similarityId; + private Integer ruleId; + private Integer problematicLineNumber; + private List locations = new ArrayList<>(); + private List vulnerabilities = new ArrayList<>(); + + public ScanIssue() { + } + + public ScanIssue(String scanIssueId, String severity, String title, String description, + String remediationAdvise, String packageVersion, String packageManager, String cve, + ScanEngine scanEngine, String filePath, String imageTag) { + this.scanIssueId = scanIssueId; + this.severity = severity; + this.title = title; + this.description = description; + this.remediationAdvise = remediationAdvise; + this.packageVersion = packageVersion; + this.packageManager = packageManager; + this.cve = cve; + this.scanEngine = scanEngine; + this.filePath = filePath; + this.imageTag = imageTag; + } + + public String getScanIssueId() { + return scanIssueId; + } + + public void setScanIssueId(String scanIssueId) { + this.scanIssueId = scanIssueId; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getRemediationAdvise() { + return remediationAdvise; + } + + public void setRemediationAdvise(String remediationAdvise) { + this.remediationAdvise = remediationAdvise; + } + + public String getPackageVersion() { + return packageVersion; + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + public String getPackageManager() { + return packageManager; + } + + public void setPackageManager(String packageManager) { + this.packageManager = packageManager; + } + + public String getCve() { + return cve; + } + + public void setCve(String cve) { + this.cve = cve; + } + + public ScanEngine getScanEngine() { + return scanEngine; + } + + public void setScanEngine(ScanEngine scanEngine) { + this.scanEngine = scanEngine; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public String getImageTag() { + return imageTag; + } + + public void setImageTag(String imageTag) { + this.imageTag = imageTag; + } + + public String getFileType() { + return fileType; + } + + public void setFileType(String fileType) { + this.fileType = fileType; + } + + public String getSecretValue() { + return secretValue; + } + + public void setSecretValue(String secretValue) { + this.secretValue = secretValue; + } + + public String getSimilarityId() { + return similarityId; + } + + public void setSimilarityId(String similarityId) { + this.similarityId = similarityId; + } + + public Integer getRuleId() { + return ruleId; + } + + public void setRuleId(Integer ruleId) { + this.ruleId = ruleId; + } + + public Integer getProblematicLineNumber() { + return problematicLineNumber; + } + + public void setProblematicLineNumber(Integer problematicLineNumber) { + this.problematicLineNumber = problematicLineNumber; + } + + public List getLocations() { + return locations; + } + + public void setLocations(List locations) { + this.locations = locations; + } + + public List getVulnerabilities() { + return vulnerabilities; + } + + public void setVulnerabilities(List vulnerabilities) { + this.vulnerabilities = vulnerabilities; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java new file mode 100644 index 00000000..d7e828ec --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java @@ -0,0 +1,82 @@ +package com.checkmarx.eclipse.devassist.model; + +/** + * Represents a vulnerability associated with a scan issue. + * Provides additional insights into the security risk. + */ +public class Vulnerability { + + private String vulnerabilityId; + private String severity; + private String title; + private String description; + private String actualValue; + private String cve; + private String fixVersion; + + public Vulnerability() { + } + + public Vulnerability(String vulnerabilityId, String severity, String title, String description) { + this.vulnerabilityId = vulnerabilityId; + this.severity = severity; + this.title = title; + this.description = description; + } + + public String getVulnerabilityId() { + return vulnerabilityId; + } + + public void setVulnerabilityId(String vulnerabilityId) { + this.vulnerabilityId = vulnerabilityId; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getActualValue() { + return actualValue; + } + + public void setActualValue(String actualValue) { + this.actualValue = actualValue; + } + + public String getCve() { + return cve; + } + + public void setCve(String cve) { + this.cve = cve; + } + + public String getFixVersion() { + return fixVersion; + } + + public void setFixVersion(String fixVersion) { + this.fixVersion = fixVersion; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java new file mode 100644 index 00000000..8b047444 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java @@ -0,0 +1,103 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Static factory for creating ProblemDescriptor objects. + * + * Encapsulates logic for: + * - Formatting problem descriptions + * - Creating appropriate fixes for issues + * - Building ProblemDescriptor instances + * + * Mirrors JetBrains ProblemBuilder. + * Cannot be instantiated. + */ +public final class ProblemBuilder { + + private ProblemBuilder() { + } + + /** + * Build a ProblemDescriptor from a scan issue. + * + * Mirrors JetBrains ProblemBuilder.build(). + * + * @param problemHelper Context with file, document, etc. + * @param scanIssue The scan issue to describe + * @param problemLineNumber Line number where problem was found + * @return ProblemDescriptor with formatted description and fixes + */ + public static ProblemDescriptor build( + ProblemHelper problemHelper, + ScanIssue scanIssue, + int problemLineNumber) { + + String description = formatDescription(scanIssue); + List fixes = createFixes(scanIssue); + + return ProblemDescriptor.builder() + .file(problemHelper.getFile()) + .scanIssue(scanIssue) + .lineNumber(problemLineNumber) + .description(description) + .fixes(fixes) + .build(); + } + + /** + * Format the problem description from scan issue details. + * + * @param scanIssue The scan issue + * @return HTML-formatted description for display + */ + private static String formatDescription(ScanIssue scanIssue) { + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append("").append(escapeHtml(scanIssue.getTitle())).append(""); + sb.append("
"); + sb.append("Severity: ").append(scanIssue.getSeverity()); + sb.append("
"); + if (scanIssue.getDescription() != null && !scanIssue.getDescription().isEmpty()) { + sb.append(escapeHtml(scanIssue.getDescription())); + } + sb.append(""); + return sb.toString(); + } + + /** + * Escape HTML special characters for safe display. + * + * @param text Text to escape + * @return HTML-escaped text + */ + private static String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + /** + * Create fixes for a scan issue. + * + * Currently creates: ViewDetailsFix + * Can be extended with: IgnoreVulnerabilityFix, etc. + * + * @param scanIssue The scan issue + * @return List of fixes (currently all as Object, can be typed later) + */ + private static List createFixes(ScanIssue scanIssue) { + List fixes = new ArrayList<>(); + // Future: add ViewDetailsFix, IgnoreVulnerabilityFix, etc. + return fixes; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java new file mode 100644 index 00000000..75293f5a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -0,0 +1,643 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Renders scan results as editor decorations. + * + * Creates visual indicators for issues in the editor: + * - Gutter icons (severity indicators on line numbers) + * - Line highlighting (background color by severity) + * - Annotations (squiggly underlines and tooltips) + * + * Integrates with Eclipse's SourceViewerConfiguration to display + * issue markers alongside the editor content. + */ +public class ProblemDecorator { + + private static final String LOG_TAG = "[SCAN-DECORATOR]"; + + // Track annotations we've created so we can remove them later + private static final Map> fileAnnotations = + new HashMap<>(); + + /** + * Render scan results as annotations in the editor. + * + * Creates FindingsAnnotation objects for each issue and adds them + * to the editor's annotation model for visual display. + * + * @param file File that was scanned + * @param scanIssues Issues to visualize + */ + public static void decorateEditor(IFile file, List scanIssues) { + if (file == null) { + return; + } + if (scanIssues == null) { + scanIssues = List.of(); + } + + // **FIX: Use getLocation() (absolute path) for consistency with RealTimeScanJob and ResultPublisher** + // This ensures fileAnnotations map keys match the same path format used throughout the codebase + String filePath = file.getLocation().toOSString(); + + try { + // Find open editor for this file + ITextEditor editor = findOpenEditor(file); + if (editor == null) { + CxLogger.info(LOG_TAG + "No open editor for: " + filePath); + return; + } + + // Get annotation model from editor + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + + if (annotationModel == null) { + CxLogger.warning(LOG_TAG + "No annotation model available"); + return; + } + + // Remove previous annotations for this file (BEFORE isEmpty check) + // This ensures stale annotations are cleared even if file is now clean + clearAnnotations(filePath, annotationModel); + + // Early return if no issues to add + if (scanIssues.isEmpty()) { + return; + } + + // Add new annotations for each issue + List annotations = new java.util.ArrayList<>(); + + for (ScanIssue issue : scanIssues) { + try { + FindingsAnnotation annotation = createAnnotation(editor, issue); + if (annotation != null) { + annotation.addButton(filePath, null); + annotations.add(annotation); + + CxLogger.info(LOG_TAG + " ─────────────────────────────────────────────────"); + CxLogger.info(LOG_TAG + " Issue: " + issue.getTitle()); + CxLogger.info(LOG_TAG + " Engine: " + issue.getScanEngine()); + CxLogger.info(LOG_TAG + " Severity: " + issue.getSeverity()); + + // **OSS-SPECIFIC LOGIC: Only decorate the first line (used for redirection)** + // For OSS issues, decorate only the first location's line to keep it simple + org.eclipse.jface.text.Position pos = null; + + if (issue.getScanEngine() != null && + issue.getScanEngine().name().equalsIgnoreCase("OSS")) { + // OSS: Decorate only the first line where package is declared + pos = decorateOssFirstLineOnly(editor, issue); + } else { + // Other engines: Use standard range calculation + pos = calculateRange(editor, issue); + } + + if (pos != null && pos.getLength() > 0) { + CxLogger.info(LOG_TAG + " Calculated Position for Editor:"); + CxLogger.info(LOG_TAG + " Offset: " + pos.getOffset()); + CxLogger.info(LOG_TAG + " Length: " + pos.getLength()); + CxLogger.info(LOG_TAG + " Range: [" + pos.getOffset() + "-" + (pos.getOffset() + pos.getLength()) + "]"); + + // Add annotation to model for display + annotationModel.addAnnotation(annotation, pos); + CxLogger.info(LOG_TAG + "Annotation added to model"); + } else { + CxLogger.warning(LOG_TAG + "FAILED: Invalid position (offset=" + + (pos != null ? pos.getOffset() : "null") + ", length=" + + (pos != null ? pos.getLength() : "null") + ")"); + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating annotation: " + + e.getMessage()); + e.printStackTrace(); + } + } + + // Store annotations for later cleanup + fileAnnotations.put(filePath, annotations); + + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + CxLogger.info(LOG_TAG + "COMPLETE: Added " + annotations.size() + + " annotations to editor"); + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error decorating editor: " + + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Create a FindingsAnnotation for a scan issue. + * + * FindingsAnnotation extends Eclipse's Annotation class and provides + * custom rendering (color, icon, tooltip) based on issue severity. + * + * @param editor Text editor + * @param issue Scan issue + * @return FindingsAnnotation, or null if creation fails + */ + private static FindingsAnnotation createAnnotation(ITextEditor editor, + ScanIssue issue) { + try { + // Get severity from issue + String severity = issue.getSeverity(); + + // DEBUG: Log the actual severity value + CxLogger.info(LOG_TAG + " [DEBUG] Issue: " + issue.getTitle() + + " | Severity from issue: " + (severity != null ? severity : "NULL")); + + // Map severity to annotation type + String annotationType = mapSeverityToAnnotationType(severity); + + CxLogger.info(LOG_TAG + " [DEBUG] Mapped to annotation type: " + annotationType); + + // Create annotation with issue details + FindingsAnnotation annotation = new FindingsAnnotation( + annotationType, + issue.getTitle(), + issue.getDescription() + ); + return annotation; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating annotation: " + + e.getMessage()); + return null; + } + } + + + /** + * Map severity level to custom Findings annotation type. + * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. + * + * @param severity Severity string (MALICIOUS, CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN, OK, IGNORED) + * @return Annotation type constant (com.checkmarx.eclipse.findings.{severity}) + */ + private static String mapSeverityToAnnotationType(String severity) { + if (severity == null) { + return "com.checkmarx.eclipse.findings.unknown"; + } + String upper = severity.toUpperCase(); + if (upper.contains("MALICIOUS")) { + return "com.checkmarx.eclipse.findings.malicious"; + } + if (upper.contains("CRITICAL") || upper.contains("ERROR")) { + return "com.checkmarx.eclipse.findings.critical"; + } + if (upper.contains("HIGH")) { + return "com.checkmarx.eclipse.findings.high"; + } + if (upper.contains("MEDIUM")) { + return "com.checkmarx.eclipse.findings.medium"; + } + if (upper.contains("LOW") || upper.contains("INFO")) { + return "com.checkmarx.eclipse.findings.low"; + } + if (upper.contains("UNKNOWN")) { + return "com.checkmarx.eclipse.findings.unknown"; + } + if (upper.contains("OK")) { + return "com.checkmarx.eclipse.findings.ok"; + } + if (upper.contains("IGNORED")) { + return "com.checkmarx.eclipse.findings.ignored"; + } + + return "com.checkmarx.eclipse.findings.unknown"; + } + + /** + * Decorate only the first line for OSS issues (package declaration line). + * + * For OSS vulnerabilities, the Location has the exact character range, + * but it may span the entire dependency block. We simplify by decorating + * only the first line where the package is declared. + * + * @param editor Text editor + * @param issue OSS issue + * @return Position covering the entire first line, or null if unable to determine + */ + private static org.eclipse.jface.text.Position decorateOssFirstLineOnly( + ITextEditor editor, ScanIssue issue) { + + try { + org.eclipse.jface.text.IDocument document = + editor.getDocumentProvider().getDocument(editor.getEditorInput()); + + if (document == null) { + CxLogger.warning(LOG_TAG + " [OSS] Document is null!"); + return null; + } + + // Get the line number from first location + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + CxLogger.warning(LOG_TAG + " [OSS] No locations found!"); + return null; + } + + com.checkmarx.eclipse.devassist.model.Location location = + issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // Convert to 0-based + + int docLength = document.getLength(); + int lineCount = document.getNumberOfLines(); + + // Bounds check + if (lineNumber < 0 || lineNumber >= lineCount) { + CxLogger.warning(LOG_TAG + " [OSS] Line " + (lineNumber + 1) + + " out of bounds (doc has " + lineCount + " lines)"); + return null; + } + + // Get the entire line information + org.eclipse.jface.text.IRegion lineInfo = document.getLineInformation(lineNumber); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + + // For OSS, decorate the entire line (excluding trailing newline) + // This ensures the gutter icon and underline appear on the whole line + int decorationLength = lineLength; + if (decorationLength == 0) { + decorationLength = 1; // Minimum 1 char + } + + CxLogger.info(LOG_TAG + " [OSS] Line " + (lineNumber + 1) + + " (offset=" + lineOffset + ", length=" + decorationLength + ")"); + + // Bounds check final position + if (lineOffset < 0 || lineOffset > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Line offset " + lineOffset + + " out of bounds (doc length=" + docLength + ")"); + return null; + } + + if (lineOffset + decorationLength > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Adjusted length from " + + decorationLength + " to " + (docLength - lineOffset)); + decorationLength = docLength - lineOffset; + } + + if (decorationLength <= 0) { + CxLogger.warning(LOG_TAG + " [OSS] Invalid decoration length: " + decorationLength); + return null; + } + + CxLogger.info(LOG_TAG + " [OSS] Decorating first line: [" + lineOffset + + "-" + (lineOffset + decorationLength) + "] = " + decorationLength + " chars"); + + return new org.eclipse.jface.text.Position(lineOffset, decorationLength); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " [OSS] Error decorating first line: " + + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * Calculate the precise source range for an annotation. + * + * Handles BOTH absolute and line-relative offsets depending on scanner: + * - Secrets API: Returns RealtimeLocation with ABSOLUTE document offsets + * - ASCA API: Returns character positions that are LINE-RELATIVE offsets + * + * @param editor Text editor + * @param issue Scan issue with location info + * @return org.eclipse.jface.text.Position representing the precise range + */ + private static org.eclipse.jface.text.Position calculateRange(ITextEditor editor, ScanIssue issue) { + try { + IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + if (document == null) return new org.eclipse.jface.text.Position(0, 1); + + int docLength = document.getLength(); + + // 1. Precise location-based offset calculation + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location location = issue.getLocations().get(0); + int rawStart = location.getStartIndex(); + int rawEnd = location.getEndIndex(); + int line = Math.max(0, location.getLine() - 1); + + IRegion lineInfo = document.getLineInformation(line); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + + int trimIndent = getLeadingWhitespaceOffset(document, lineOffset, lineLength); + // Use explicit flag from Location instead of inferring from magnitude + boolean isAbsoluteOffset = location.isAbsoluteOffset(); + + int charStart = isAbsoluteOffset ? rawStart : (lineOffset + rawStart); + int charEnd = isAbsoluteOffset ? rawEnd : (lineOffset + rawEnd); + + // If start points to the beginning of the line, shift past leading whitespace + if (charStart <= lineOffset) { + charStart = lineOffset + trimIndent; + } + + if (charEnd <= charStart) { + charEnd = lineOffset + lineLength; + } + + // Clamp offsets safely within document bounds + charStart = Math.max(0, Math.min(charStart, docLength)); + charEnd = Math.max(charStart, Math.min(charEnd, docLength)); + + if (charEnd > charStart) { + return new org.eclipse.jface.text.Position(charStart, charEnd - charStart); + } + } + + // 2. Fallback: Highlight line content (skipping leading indentation) + int targetLine = 0; + if (issue.getProblematicLineNumber() != null) { + targetLine = issue.getProblematicLineNumber() - 1; + } else if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + targetLine = issue.getLocations().get(0).getLine() - 1; + } + + int line = Math.max(0, Math.min(targetLine, document.getNumberOfLines() - 1)); + IRegion lineInfo = document.getLineInformation(line); + + int trimIndent = getLeadingWhitespaceOffset(document, lineInfo.getOffset(), lineInfo.getLength()); + int startOffset = lineInfo.getOffset() + trimIndent; + int length = Math.max(1, lineInfo.getLength() - trimIndent); + + return new org.eclipse.jface.text.Position(startOffset, length); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error calculating range: " + e.getMessage()); + return new org.eclipse.jface.text.Position(0, 1); + } + } + /** + * Calculates the number of leading whitespace characters (spaces/tabs) on a given line. + * + * @param document Text document + * @param lineOffset Start character offset of the line + * @param lineLength Total length of the line + * @return Number of leading whitespace characters + */ + private static int getLeadingWhitespaceOffset(org.eclipse.jface.text.IDocument document, + int lineOffset, + int lineLength) { + try { + String lineText = document.get(lineOffset, lineLength); + int leadingSpaces = 0; + + while (leadingSpaces < lineText.length() && + Character.isWhitespace(lineText.charAt(leadingSpaces))) { + leadingSpaces++; + } + + return leadingSpaces; + } catch (Exception e) { + return 0; + } + } + + /** + * Clear previous annotations for a file. + * + * @param filePath File path + * @param annotationModel Annotation model + */ + private static void clearAnnotations(String filePath, + IAnnotationModel annotationModel) { + + try { + List previousAnnotations = fileAnnotations.get(filePath); + if (previousAnnotations != null) { + for (Annotation annotation : previousAnnotations) { + annotationModel.removeAnnotation(annotation); + } + fileAnnotations.remove(filePath); + + CxLogger.info(LOG_TAG + " Cleared " + previousAnnotations.size() + + " previous annotations"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing annotations: " + + e.getMessage()); + } + } + + /** + * Find open text editor for a file. + * + * @param file File to find editor for + * @return ITextEditor or null + */ + private static ITextEditor findOpenEditor(IFile file) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + + IWorkbenchPage page = null; + try { + page = workbench.getActiveWorkbenchWindow().getActivePage(); + } catch (NullPointerException e) { + // Workbench window not available, try all windows + for (var window : workbench.getWorkbenchWindows()) { + page = window.getActivePage(); + if (page != null) break; + } + } + + if (page == null) { + return null; + } + + var editors = page.getEditors(); + for (var editor : editors) { + Object input = editor.getEditorInput(); + if (input instanceof org.eclipse.ui.IFileEditorInput) { + IFile editorFile = ((org.eclipse.ui.IFileEditorInput) input) + .getFile(); + if (editorFile.equals(file)) { + // Try method 1: Direct ITextEditor instance + if (editor instanceof ITextEditor) { + return (ITextEditor) editor; + } + + // Try method 2: ITextEditor adapter (for MavenPomEditor, etc.) + ITextEditor textEditor = editor.getAdapter(ITextEditor.class); + if (textEditor != null) { + return textEditor; + } + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error finding open editor: " + + e.getMessage()); + } + + return null; + } + + /** + * Remove all decorations for a file. + * + * Called when: + * - Results are cleared + * - File is closed + * - Editor is disposed + * + * @param file File to remove decorations from + */ + public static void clearDecorations(IFile file) { + try { + // **FIX: Use getLocation() (absolute path) for consistency with decorateEditor()** + // Ensures fileAnnotations map lookups use the same path format + String filePath = file.getLocation().toOSString(); + CxLogger.info(LOG_TAG + " Clearing decorations for: " + filePath); + + ITextEditor editor = findOpenEditor(file); + if (editor == null) { + fileAnnotations.remove(filePath); + return; + } + + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + + if (annotationModel != null) { + clearAnnotations(filePath, annotationModel); + } + + CxLogger.info(LOG_TAG + " Decorations cleared"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing decorations: " + + e.getMessage()); + } + } + + /** + * Get decorator statistics. + * + * @return Summary string + */ + public static String getStatistics() { + int totalAnnotations = fileAnnotations.values().stream() + .mapToInt(List::size) + .sum(); + return "Decorated files: " + fileAnnotations.size() + + ", Total annotations: " + totalAnnotations; + } + + /** + * Highlight a line and add gutter icon for a problem. + * + * Delegates to the decorateEditor() path which handles annotation creation + * and display in the editor's gutter and line highlighting. + * + * @param problemHelper Problem helper with context (used to locate the file being edited) + * @param scanIssue Scan issue to highlight + * @param isProblem Whether this is a problem (not just note) + * @param problemLineNumber Line number to highlight + */ + public void highlightLineAddGutterIconForProblem( + ProblemHelper problemHelper, + ScanIssue scanIssue, + boolean isProblem, + int problemLineNumber) { + + if (!isProblem || scanIssue == null) { + return; + } + + try { + // Get the file from problem helper and decorate it + // Wrap single issue in a list and delegate to decorateEditor() + IFile file = problemHelper.getFile(); + if (file != null && file.exists()) { + decorateEditor(file, List.of(scanIssue)); + } else { + CxLogger.warning(LOG_TAG + " Cannot decorate: file not found or null"); + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error in highlightLineAddGutterIconForProblem: " + e.getMessage(), e); + } + } + + /** + * Remove all highlighters/decorations from a project. + * + * Called by DevAssistInspectionMgr when resetting editor state. + * Clears all tracked annotations across all files. + * + * @param project Project to clear (used for context, actual clearing is project-wide) + */ + public static void removeAllHighlighters(org.eclipse.core.resources.IProject project) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + CxLogger.info(LOG_TAG + " removeAllHighlighters: Workbench not available"); + return; + } + + for (org.eclipse.ui.IWorkbenchWindow window : workbench.getWorkbenchWindows()) { + for (IWorkbenchPage page : window.getPages()) { + for (org.eclipse.ui.IEditorReference ref : page.getEditorReferences()) { + try { + ITextEditor editor = (ITextEditor) ref.getEditor(false); + if (editor != null) { + IAnnotationModel annotationModel = + editor.getDocumentProvider().getAnnotationModel(editor.getEditorInput()); + if (annotationModel != null) { + for (List annotations : fileAnnotations.values()) { + for (Annotation ann : annotations) { + try { + annotationModel.removeAnnotation(ann); + } catch (Exception e) { + // Continue removing others + } + } + } + } + } + } catch (Exception e) { + // Continue with other editors + } + } + } + } + + fileAnnotations.clear(); + CxLogger.info(LOG_TAG + " Removed all highlighters for project: " + project.getName()); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error removing all highlighters: " + e.getMessage(), e); + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java new file mode 100644 index 00000000..dee7d2a3 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java @@ -0,0 +1,117 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.List; + +import org.eclipse.core.resources.IFile; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Eclipse equivalent of JetBrains ProblemDescriptor. + * + * Represents a detected problem/issue in a file with: + * - Issue metadata (file, scan issue, line number) + * - Human-readable description + * - Associated fixes for the problem + * + * Option B: Full structure with fixes list, mirroring JetBrains. + */ +public class ProblemDescriptor { + + private final IFile file; + private final ScanIssue scanIssue; + private final int lineNumber; + private final String description; + private final List fixes; + + /** + * Constructor for ProblemDescriptor. + * + * @param file The file being analyzed + * @param scanIssue The scan issue + * @param lineNumber Line number of the issue + * @param description Human-readable description + * @param fixes Associated fixes + */ + public ProblemDescriptor(IFile file, ScanIssue scanIssue, int lineNumber, + String description, List fixes) { + this.file = file; + this.scanIssue = scanIssue; + this.lineNumber = lineNumber; + this.description = description; + this.fixes = fixes; + } + + public IFile getFile() { + return file; + } + + public ScanIssue getScanIssue() { + return scanIssue; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getDescription() { + return description; + } + + public List getFixes() { + return fixes; + } + + /** + * Get the problem fixes as an array. + * + * @return Array of fixes (or empty array if none) + */ + public Object[] getFixesArray() { + return fixes != null ? fixes.toArray() : new Object[0]; + } + + /** + * Builder for ProblemDescriptor. + */ + public static class Builder { + private IFile file; + private ScanIssue scanIssue; + private int lineNumber; + private String description; + private List fixes; + + public Builder file(IFile file) { + this.file = file; + return this; + } + + public Builder scanIssue(ScanIssue scanIssue) { + this.scanIssue = scanIssue; + return this; + } + + public Builder lineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder fixes(List fixes) { + this.fixes = fixes; + return this; + } + + public ProblemDescriptor build() { + return new ProblemDescriptor(file, scanIssue, lineNumber, description, fixes); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java new file mode 100644 index 00000000..19e9717b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java @@ -0,0 +1,174 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Helper class that aggregates all context needed for problem processing. + * + * Holds: file, project, document, scanners, issues, holder service, decorator. + * Used by orchestration flow to pass context to various processing stages. + * + * Mirrors JetBrains ProblemHelper with Eclipse types. + */ +public class ProblemHelper { + + private final IFile file; + private final IProject project; + private final String filePath; + private final IDocument document; + private final List supportedScanners; + private final List scanIssueList; + private final ProblemHolderService problemHolderService; + private final ProblemDecorator problemDecorator; + + /** + * Constructor for ProblemHelper. + */ + public ProblemHelper(IFile file, IProject project, String filePath, + IDocument document, List supportedScanners, + List scanIssueList, ProblemHolderService problemHolderService, + ProblemDecorator problemDecorator) { + this.file = file; + this.project = project; + this.filePath = filePath; + this.document = document; + this.supportedScanners = supportedScanners; + this.scanIssueList = scanIssueList; + this.problemHolderService = problemHolderService; + this.problemDecorator = problemDecorator; + } + + public IFile getFile() { + return file; + } + + public IProject getProject() { + return project; + } + + public String getFilePath() { + return filePath; + } + + public IDocument getDocument() { + return document; + } + + public List getSupportedScanners() { + return supportedScanners; + } + + public List getScanIssueList() { + return scanIssueList; + } + + public ProblemHolderService getProblemHolderService() { + return problemHolderService; + } + + public ProblemDecorator getProblemDecorator() { + return problemDecorator; + } + + /** + * Builder method enforcing mandatory fields: file, project. + * + * Mirrors JetBrains ProblemHelper.builder(PsiFile, Project). + * + * @param file IFile to process + * @param project IProject containing the file + * @return Builder with file and project set + * @throws IllegalArgumentException if file or project is null + */ + public static Builder builder(IFile file, IProject project) { + if (Objects.isNull(file) || Objects.isNull(project)) { + throw new IllegalArgumentException( + "Mandatory fields required: file, project"); + } + return new Builder() + .file(file) + .project(project); + } + + /** + * Create a new builder from this ProblemHelper. + * + * @return Builder with all fields from this instance + */ + public Builder toBuilder() { + return builder(this.file, this.project) + .filePath(this.filePath) + .document(this.document) + .supportedScanners(this.supportedScanners) + .scanIssueList(this.scanIssueList) + .problemHolderService(this.problemHolderService) + .problemDecorator(this.problemDecorator); + } + + /** + * Builder for ProblemHelper. + */ + public static class Builder { + private IFile file; + private IProject project; + private String filePath; + private IDocument document; + private List supportedScanners; + private List scanIssueList; + private ProblemHolderService problemHolderService; + private ProblemDecorator problemDecorator; + + public Builder file(IFile file) { + this.file = file; + return this; + } + + public Builder project(IProject project) { + this.project = project; + return this; + } + + public Builder filePath(String filePath) { + this.filePath = filePath; + return this; + } + + public Builder document(IDocument document) { + this.document = document; + return this; + } + + public Builder supportedScanners(List supportedScanners) { + this.supportedScanners = supportedScanners; + return this; + } + + public Builder scanIssueList(List scanIssueList) { + this.scanIssueList = scanIssueList; + return this; + } + + public Builder problemHolderService(ProblemHolderService problemHolderService) { + this.problemHolderService = problemHolderService; + return this; + } + + public Builder problemDecorator(ProblemDecorator problemDecorator) { + this.problemDecorator = problemDecorator; + return this; + } + + public ProblemHelper build() { + return new ProblemHelper(file, project, filePath, document, + supportedScanners, scanIssueList, problemHolderService, problemDecorator); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java new file mode 100644 index 00000000..f6d2e627 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -0,0 +1,276 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.e4.core.services.events.IEventBroker; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.utils.PluginUtils; + +/** + * In-memory cache for scan results (ScanIssue), keyed by file path. + * + * Thread-safe via ConcurrentHashMap. Used to avoid redundant scans + * and to enable result restoration when files are reopened. + * + * Mirrors JetBrains ProblemHolderService pattern with Eclipse IEventBroker for notifications. + */ +public class ProblemHolderService { + + private static final String LOG_TAG = "[PROBLEM-HOLDER]"; + public static final String ISSUES_UPDATED_TOPIC = "com/checkmarx/issues/updated"; + + // Session property key for storing service in project + public static final String SERVICE_KEY = ProblemHolderService.class.getName() + + ".INSTANCE"; + + private final ConcurrentHashMap> fileToScanIssues = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap> fileToProblemDescriptors = + new ConcurrentHashMap<>(); + + /** + * Returns the instance of this service for the given project. + * + * @param project the project. + * @return the instance of this service for the given project. + */ + public static ProblemHolderService getInstance(IProject project) { + if (project == null) { + return null; + } + try { + org.eclipse.core.runtime.QualifiedName key = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "problem-holder-service"); + ProblemHolderService instance = (ProblemHolderService) project.getSessionProperty(key); + if (instance == null) { + instance = new ProblemHolderService(); + project.setSessionProperty(key, instance); + } + return instance; + } catch (Exception e) { + return new ProblemHolderService(); + } + } + + /** + * Cache scan issues for a file. + * + * @param filePath Absolute file path + * @param issues Issues found by scanners + */ + public void addScanIssues(String filePath, List issues) { + if (filePath == null || issues == null) { + return; + } + fileToScanIssues.put(filePath, new ArrayList<>(issues)); + // **KEY: Notify all listeners of the update (JetBrains pattern)** + publishIssuesUpdated(); + } + + /** + * Get cached scan issues for a file. + * + * @param filePath Absolute file path + * @return Cached issues or empty list + */ + public List getScanIssuesByFile(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + + List cached = fileToScanIssues.get(filePath); + return cached != null ? new ArrayList<>(cached) : Collections.emptyList(); + } + + /** + * Get all cached issues across all files. + * + * @return Map of file path → issues + */ + public Map> getAllScanIssues() { + + Map> result = new HashMap<>(); + for (Map.Entry> entry : fileToScanIssues.entrySet()) { + result.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + int totalIssues = result.values().stream().mapToInt(List::size).sum(); + return result; + } + + /** + * Merge new issues with existing issues for a file. + * Deduplicates by issue ID. + * + * @param filePath Absolute file path + * @param newIssues Issues to merge + */ + public void mergeScanIssues(String filePath, List newIssues) { + if (filePath == null || newIssues == null) { + return; + } + + List existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + Map merged = new HashMap<>(); + + // Add existing issues + for (ScanIssue issue : existing) { + merged.put(issue.getScanIssueId(), issue); + } + + // Add/override with new issues (by ID) + for (ScanIssue issue : newIssues) { + merged.put(issue.getScanIssueId(), issue); + } + + fileToScanIssues.put(filePath, new ArrayList<>(merged.values())); + CxLogger.info(LOG_TAG + " Merged " + newIssues.size() + " issues for: " + filePath); + + // **KEY: Notify listeners when cache is modified** + publishIssuesUpdated(); + } + + /** + * Clear cached issues for a file. + * + * @param filePath Absolute file path + */ + public void removeScanIssues(String filePath) { + if (filePath == null) { + return; + } + + fileToScanIssues.remove(filePath); + CxLogger.info(LOG_TAG + " Cleared cache for: " + filePath); + } + + /** + * Remove cached scan issues for a specific scanner type and file. + * Mirrors JetBrains DevAssistScanScheduler.cacheScanResults() pattern. + * + * When a partial re-scan is performed (e.g., only ASCA is rescanned), + * this method removes the old results for THAT scanner type before + * merging the new results. + * + * @param scannerType Name of the scanner engine (e.g., "ASCA", "OSS", "IaC") + * @param filePath Absolute file path + */ + public void removeScanIssuesByFileAndScanner(String scannerType, String filePath) { + if (filePath == null || scannerType == null) { + return; + } + + List existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + List filtered = new ArrayList<>(); + + // Keep only issues from OTHER scanners + for (ScanIssue issue : existing) { + if (issue.getScanEngine() != null && + !issue.getScanEngine().name().equals(scannerType)) { + filtered.add(issue); + } + } + + fileToScanIssues.put(filePath, filtered); + CxLogger.info(LOG_TAG + " Removed " + scannerType + " issues for: " + filePath + + " (kept " + filtered.size() + " issues from other scanners)"); + } + + /** + * Clear all caches (on project close). + */ + public void clearAll() { + fileToScanIssues.clear(); + CxLogger.info(LOG_TAG + " All caches cleared"); + } + + /** + * Get cache statistics for debugging. + * + * @return Summary string + */ + public String getCacheStats() { + int fileCount = fileToScanIssues.size(); + int totalIssues = fileToScanIssues.values().stream() + .mapToInt(List::size) + .sum(); + return "Files: " + fileCount + ", Total Issues: " + totalIssues; + } + + /** + * Publish issues update via Eclipse IEventBroker. + * Subscribers listen on ISSUES_UPDATED_TOPIC using @UIEventTopic annotation. + * + * @see com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView + */ + private void publishIssuesUpdated() { + try { + IEventBroker eventBroker = PluginUtils.getEventBroker(); + if (eventBroker != null) { + Map> allIssues = getAllScanIssues(); + + eventBroker.post(ISSUES_UPDATED_TOPIC, allIssues); + } else { + System.err.println(LOG_TAG + " [EVENT-BROKER] ✗ EventBroker not available"); + } + } catch (Exception e) { + System.err.println(LOG_TAG + " [EVENT-BROKER] Error publishing event: " + e.getMessage()); + e.printStackTrace(); + } + } + + public static void addToCxOneFindings(IFile file, List problemsList) { + getInstance(file.getProject()).addScanIssues(file.getFullPath().toOSString(), problemsList); + } + + /** + * Cache problem descriptors for a file. + * + * @param filePath Absolute file path + * @param descriptors Problem descriptors to cache + */ + public void addProblemDescriptors(String filePath, List descriptors) { + if (filePath == null || descriptors == null) { + return; + } + fileToProblemDescriptors.put(filePath, new ArrayList<>(descriptors)); + CxLogger.info(LOG_TAG + " Cached " + descriptors.size() + " problem descriptors for: " + filePath); + } + + /** + * Get cached problem descriptors for a file. + * + * @param filePath Absolute file path + * @return Cached problem descriptors or empty list + */ + public List getProblemDescriptors(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + List cached = fileToProblemDescriptors.get(filePath); + return cached != null ? Collections.unmodifiableList(cached) : Collections.emptyList(); + } + + /** + * Remove cached problem descriptors for a file. + * + * @param filePath Absolute file path + */ + public void removeProblemDescriptorsForFile(String filePath) { + if (filePath == null) { + return; + } + fileToProblemDescriptors.remove(filePath); + CxLogger.info(LOG_TAG + " Removed problem descriptors for: " + filePath); + } + +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java new file mode 100644 index 00000000..7d7a9088 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java @@ -0,0 +1,223 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Processor that validates individual scan issues and creates problem descriptors. + * + * Encapsulates logic for: + * - Validating scan issue data (location, line, severity) + * - Creating problem descriptors for valid issues + * - Triggering decoration for highlighted issues + * + * CRITICAL: Prevents crashes from invalid data by validating before processing. + * + * Mirrors JetBrains ScanIssueProcessor. + */ +public class ScanIssueProcessor { + + private static final String LOG_TAG = "[SCAN-ISSUE-PROCESSOR]"; + + private final IFile file; + private final IDocument document; + private final ProblemHelper problemHelper; + + /** + * Constructor that takes file, document, and problemHelper. + * + * @param file The file being processed + * @param document The document + * @param problemHelper Problem helper with context + */ + public ScanIssueProcessor(IFile file, IDocument document, ProblemHelper problemHelper) { + this.file = file; + this.document = document; + this.problemHelper = problemHelper; + } + + /** + * Alternate constructor that extracts file and document from ProblemHelper. + * + * Mirrors JetBrains ScanIssueProcessor(ProblemHelper). + * + * @param problemHelper Problem helper containing file, document, etc. + */ + public ScanIssueProcessor(ProblemHelper problemHelper) { + this.file = problemHelper.getFile(); + this.document = problemHelper.getDocument(); + this.problemHelper = problemHelper; + } + + /** + * Process a single scan issue and create a problem descriptor if valid. + * + * Validation pipeline: + * 1. Check location exists and is not empty + * 2. Extract line number from location + * 3. Check line is within document range + * 4. Check severity is present and not blank + * 5. If all valid: create problem descriptor + * 6. If decorator enabled: highlight the issue + * + * Mirrors JetBrains ScanIssueProcessor.processScanIssue(). + * + * @param scanIssue Scan issue to process + * @param isDecoratorEnabled Whether to add visual decorations + * @return ProblemDescriptor if valid, null if invalid + */ + public ProblemDescriptor processScanIssue(ScanIssue scanIssue, boolean isDecoratorEnabled) { + + // Validation: location exists and is not empty + if (!isValidLocation(scanIssue)) { + CxLogger.info(LOG_TAG + " Invalid location for: " + scanIssue.getTitle()); + return null; + } + + // Extract line number + int problemLineNumber = scanIssue.getLocations().get(0).getLine(); + + // Validation: line number and severity are valid + if (!isValidLineAndSeverity(problemLineNumber, scanIssue)) { + CxLogger.info(LOG_TAG + " Invalid line/severity for: " + scanIssue.getTitle() + + " (line=" + problemLineNumber + ", severity=" + scanIssue.getSeverity() + ")"); + return null; + } + + try { + return processValidIssue(scanIssue, problemLineNumber, isDecoratorEnabled); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Exception processing issue: " + + scanIssue.getTitle() + ": " + e.getMessage(), e); + return null; + } + } + + /** + * Validate that scan issue has a location. + * + * @param scanIssue Scan issue to validate + * @return true if location exists and is not empty + */ + private boolean isValidLocation(ScanIssue scanIssue) { + return scanIssue.getLocations() != null && !scanIssue.getLocations().isEmpty(); + } + + /** + * Validate line number and severity. + * + * @param lineNumber Line number to check + * @param scanIssue Scan issue with severity + * @return true if line is in range and severity is not blank + */ + private boolean isValidLineAndSeverity(int lineNumber, ScanIssue scanIssue) { + // Check line is within document bounds + if (isLineOutOfRange(lineNumber)) { + return false; + } + // Check severity is present and not blank + return scanIssue.getSeverity() != null && !scanIssue.getSeverity().isBlank(); + } + + /** + * Check if line number is outside document range. + * + * @param lineNumber Line number to check + * @return true if line is out of range + */ + private boolean isLineOutOfRange(int lineNumber) { + try { + int lineCount = document.getNumberOfLines(); + return lineNumber < 1 || lineNumber > lineCount; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error checking line range: " + e.getMessage(), e); + return true; + } + } + + /** + * Process a valid scan issue. + * + * 1. Check if it's a "problem" (not just info/note) + * 2. If problem: create problem descriptor via ProblemBuilder + * 3. If decorator enabled: highlight the issue + * + * @param scanIssue The valid scan issue + * @param problemLineNumber Line number (already validated) + * @param isDecoratorEnabled Whether to decorate + * @return ProblemDescriptor if it's a problem, null if just info + */ + private ProblemDescriptor processValidIssue( + ScanIssue scanIssue, + int problemLineNumber, + boolean isDecoratorEnabled) { + + boolean isProblem = isProblem(scanIssue.getSeverity().toLowerCase()); + + ProblemDescriptor problemDescriptor = null; + if (isProblem) { + problemDescriptor = createProblemDescriptor(scanIssue, problemLineNumber); + } + + if (isDecoratorEnabled) { + highlightIssueIfNeeded(scanIssue, problemLineNumber, isProblem); + } + + return problemDescriptor; + } + + /** + * Check if severity indicates a reportable problem. + * Matches severity table in ProblemDecorator.mapSeverityToAnnotationType(). + * + * @param severity Severity string (lowercase) + * @return true if problem, false if info/note/unknown/ok/ignored + */ + private boolean isProblem(String severity) { + return severity.equals("malicious") || + severity.equals("critical") || + severity.equals("high") || + severity.equals("medium") || + severity.equals("low"); + } + + /** + * Create a problem descriptor via ProblemBuilder. + * + * @param scanIssue The scan issue + * @param problemLineNumber Line number + * @return ProblemDescriptor, or null on error + */ + private ProblemDescriptor createProblemDescriptor(ScanIssue scanIssue, int problemLineNumber) { + try { + return ProblemBuilder.build(problemHelper, scanIssue, problemLineNumber); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to create descriptor for: " + + scanIssue.getTitle() + ": " + e.getMessage(), e); + return null; + } + } + + /** + * Highlight the issue in the editor and add gutter icon. + * + * Delegates to ProblemDecorator to add visual decoration. + * + * @param scanIssue The scan issue + * @param problemLineNumber Line number + * @param isProblem Whether it's a problem or just note + */ + private void highlightIssueIfNeeded(ScanIssue scanIssue, int problemLineNumber, boolean isProblem) { + ProblemDecorator problemDecorator = problemHelper.getProblemDecorator(); + if (Objects.isNull(problemDecorator)) { + problemDecorator = new ProblemDecorator(); + } + problemDecorator.highlightLineAddGutterIconForProblem( + problemHelper, scanIssue, isProblem, problemLineNumber); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java new file mode 100644 index 00000000..f98b2b3a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java @@ -0,0 +1,272 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.ast.asca.ScanDetail; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Adapter class for handling ASCA scan results and converting them into a standardized format. + * + * This class wraps a ASCA {@link ScanResult} instance and provides methods to process and extract + * meaningful scan issues based on ASCA findings detected in the files. + * + * Features: + * - Groups multiple vulnerabilities on the same line + * - Sorts vulnerabilities by severity precedence + * - Filters ignored vulnerabilities (optional) + * - Generates proper unique IDs + * - Tracks location information + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class AscaScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[ASCA-ADAPTOR]"; + private static final String MULTIPLE_ISSUES_SUFFIX = " ASCA issues"; + + private final com.checkmarx.ast.asca.ScanResult ascaScanResult; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of AscaScanResultAdaptor with the specified ASCA scan results. + * + * @param ascaScanResult the ASCA scan results to be wrapped + * @param filePath the path of the file being scanned + */ + public AscaScanResultAdaptor(com.checkmarx.ast.asca.ScanResult ascaScanResult, String filePath) { + this.ascaScanResult = ascaScanResult; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public com.checkmarx.ast.asca.ScanResult getResults() { + return ascaScanResult; + } + + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Builds a list of ScanIssue objects from the ASCA scan results. + * Groups multiple vulnerabilities on the same line and sorts them by severity. + */ + private List buildIssues() { + if (ascaScanResult == null || ascaScanResult.getScanDetails() == null) { + CxLogger.info(LOG_TAG + " No scan results or scan details available"); + return Collections.emptyList(); + } + + List scanDetails = ascaScanResult.getScanDetails(); + if (scanDetails.isEmpty()) { + return Collections.emptyList(); + } + + // Group scan details by line number, then sort by severity precedence + Map> groupedIssues = scanDetails.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy( + ScanDetail::getLine, + Collectors.collectingAndThen(Collectors.toList(), detailsList -> { + detailsList.sort(Comparator.comparingInt(detail -> + getSeverityPrecedence(detail.getSeverity()))); + return detailsList; + }) + )); + + List issues = groupedIssues.values().stream() + .map(this::createScanIssueForGroup) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + issues.size() + " grouped scan issues for file: " + filePath); + return issues; + } + + /** + * Creates a ScanIssue from a group of ASCA scan details that are on the same line. + * + * @param ascaScanDetails the list of ASCA scan details for the same line (already sorted by severity) + * @return a ScanIssue representing the ASCA finding(s), or null if conversion fails + */ + private ScanIssue createScanIssueForGroup(List ascaScanDetails) { + if (ascaScanDetails == null || ascaScanDetails.isEmpty()) { + return null; + } + + try { + ScanIssue scanIssue = getScanIssue(ascaScanDetails); + + // Add vulnerabilities from all details in the group + for (int i = 0; i < ascaScanDetails.size(); i++) { + ScanDetail detail = ascaScanDetails.get(i); + String vulnerabilityId = (i == 0) ? scanIssue.getScanIssueId() : null; + Vulnerability vuln = createVulnerability(detail, vulnerabilityId); + scanIssue.getVulnerabilities().add(vuln); + } + + // Update title based on actual number of vulnerabilities + updateScanIssueTitleAndLocation(scanIssue, ascaScanDetails); + + CxLogger.info(LOG_TAG + " Created ScanIssue with " + scanIssue.getVulnerabilities().size() + + " vulnerabilities on line " + scanIssue.getProblematicLineNumber()); + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert scan details group to ScanIssue: " + e.getMessage()); + return null; + } + } + + /** + * Creates a ScanIssue with appropriate title and basic properties from a group of ASCA scan details. + * + * @param ascaScanDetails the list of ASCA scan details (already sorted by severity) + * @return a ScanIssue with basic properties set + */ + private ScanIssue getScanIssue(List ascaScanDetails) { + ScanIssue scanIssue = new ScanIssue(); + ScanDetail firstDetail = ascaScanDetails.get(0); // Highest severity (already sorted) + + // Set title based on whether there are multiple issues on the same line + String title; + if (ascaScanDetails.size() > 1) { + title = ascaScanDetails.size() + MULTIPLE_ISSUES_SUFFIX; + } else { + title = firstDetail.getRuleName(); + } + + scanIssue.setTitle(title); + scanIssue.setDescription(firstDetail.getDescription()); + scanIssue.setSeverity(mapSeverity(firstDetail.getSeverity())); + scanIssue.setFilePath(filePath); + scanIssue.setScanEngine(ScanEngine.ASCA); + scanIssue.setProblematicLineNumber(firstDetail.getLine()); + scanIssue.setRuleId(firstDetail.getRuleID()); + + // Generate unique ID based on line, rule ID, and rule name + String scanIssueId = generateUniqueId(firstDetail); + scanIssue.setScanIssueId(scanIssueId); + + return scanIssue; + } + + /** + * Creates a Vulnerability object from a ASCA scan detail. + * + * @param scanDetail the ASCA scan detail + * @param overrideId optional vulnerability ID to use instead of generating one + * @return a Vulnerability object + */ + private Vulnerability createVulnerability(ScanDetail scanDetail, String overrideId) { + Vulnerability vulnerability = new Vulnerability(); + + // Generate or use provided vulnerability ID + String vulnerabilityId = generateUniqueId(scanDetail); + if (overrideId != null && !overrideId.isBlank()) { + vulnerabilityId = overrideId; + } + + vulnerability.setVulnerabilityId(vulnerabilityId); + vulnerability.setTitle(scanDetail.getRuleName()); + vulnerability.setDescription(scanDetail.getDescription()); + vulnerability.setSeverity(mapSeverity(scanDetail.getSeverity())); + + CxLogger.info(LOG_TAG + " Created vulnerability '" + scanDetail.getRuleName() + + "' with vulnerabilityId '" + vulnerabilityId + "'"); + + return vulnerability; + } + + /** + * Updates the ScanIssue title and location based on vulnerability count and scan details. + */ + private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List ascaScanDetails) { + // Update title based on actual number of vulnerabilities + if (scanIssue.getVulnerabilities().size() == 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().get(0).getTitle()); + } else if (scanIssue.getVulnerabilities().size() > 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + MULTIPLE_ISSUES_SUFFIX); + } + + // Add location information from first detail + ScanDetail firstDetail = ascaScanDetails.get(0); + Location location = new Location(); + location.setLine(firstDetail.getLine()); + scanIssue.getLocations().add(location); + } + + /** + * Maps ASCA severity levels to standardized severity strings. + * + * @param ascaSeverity the ASCA severity level + * @return standardized severity string + */ + private String mapSeverity(String ascaSeverity) { + if (ascaSeverity == null) { + return "Medium"; + } + + switch (ascaSeverity.toLowerCase()) { + case "critical": + return "Critical"; + case "high": + return "High"; + case "medium": + return "Medium"; + case "low": + return "Low"; + case "info": + return "Low"; + default: + return "Medium"; + } + } + + /** + * Get severity precedence for sorting (higher number = higher severity). + */ + private int getSeverityPrecedence(String severity) { + if (severity == null) { + return 3; + } + + switch (severity.toLowerCase()) { + case "critical": + return 5; + case "high": + return 4; + case "medium": + return 3; + case "low": + return 2; + case "info": + return 1; + default: + return 3; + } + } + + /** + * Generates a unique ID for the given scan detail. + */ + private String generateUniqueId(ScanDetail scanDetail) { + if (scanDetail != null) { + return DevAssistUtils.generateUniqueId( + scanDetail.getLine(), + scanDetail.getRuleID() + scanDetail.getRuleName(), + scanDetail.getFileName()); + } + return ScanEngine.ASCA.name(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java new file mode 100644 index 00000000..dd0076fe --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java @@ -0,0 +1,54 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * ASCA Scanner Command that manages the lifecycle of ASCA realtime scanning. + * Integrates with the scanner registry system to handle enabling/disabling of ASCA scanning. + */ +public class AscaScannerCommand extends BaseScannerCommand { + + public AscaScannerService ascaScannerService; + private static final String LOG_TAG = "[ASCA-COMMAND]"; + + /** + * Create an ASCA scanner command for a project. + * + * @param project Eclipse project + */ + public AscaScannerCommand(IProject project) { + super(project, AscaScannerService.createConfig()); + this.ascaScannerService = new AscaScannerService(project); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + @Override + public void initializeScanner() { + CxLogger.info(LOG_TAG + " Initialized for real-time scanning"); + } + + /** + * Perform an ASCA scan on a file. + * + * @param filePath File path to scan + * @param document Document content + * @return Scan result + */ + public ScanResult scan(String filePath, IDocument document) { + return ascaScannerService.scanWithDocument(filePath, document); + } + + @Override + public void dispose() { + try { + ascaScannerService.close(); + CxLogger.info(LOG_TAG + " Disposed for project: " + project.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java new file mode 100644 index 00000000..d3f951a2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -0,0 +1,351 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * ASCA (Application Source Code Analysis) scanner service. + * + * Scans source code files for vulnerabilities using the CxWrapper. Includes + * comprehensive file handling, temporary file management with security checks, + * and proper error handling. + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class AscaScannerService extends BaseScannerService { + + private static final String LOG_TAG = "[ASCA-SERVICE]"; + private static final String ASCA_DIR = "CxASCA"; + private static final Object SCAN_LOCK = new Object(); + + public AscaScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default ASCA scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.ASCA.name()) + .configSection(DevAssistConstants.ASCA_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_ASCA_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.ASCA_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.ASCA_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_ASCA_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + if (filePath == null) { + return false; + } + + String lowerPath = filePath.toLowerCase(); + for (String ext : DevAssistConstants.ASCA_SUPPORTED_EXTENSIONS) { + if (lowerPath.endsWith("." + ext)) { + return true; + } + } + return false; + } + + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + com.checkmarx.eclipse.devassist.common.ScanResult result = scanWithDocument(filePath, + liveDocument != null ? liveDocument : new Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (com.checkmarx.eclipse.devassist.common.ScanResult) result; + } + + /** + * Primary scan method - gets file content and executes scan. + */ + public com.checkmarx.eclipse.devassist.common.ScanResult scanWithDocument(String filePath, IDocument document) { + return scanInternal(filePath, document, project); + } + + @Override + public void close() throws Exception { + // No resources to close + } + + private com.checkmarx.eclipse.devassist.common.ScanResult scanInternal(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + try { + // Get file content from document or file system + String fileContent = getFileContent(filePath, document); + if (fileContent == null) { + CxLogger.warning(LOG_TAG + " Could not read file content: " + filePath); + return null; + } + // Run ASCA scan with proper temp file management + Object rawResults = runAscaScan(filePath, fileContent); + if (rawResults == null) { + return null; + } + return new AscaScanResultAdaptor((com.checkmarx.ast.asca.ScanResult) rawResults, filePath); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Scan failed: " + e.getMessage(), e); + return null; + } + } + + /** + * Get file content from document (if available) or from file system. + */ + + private String getFileContent(String filePath, IDocument document) { + // 1. Try reading from the in-memory document buffer first + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + if (filePath == null || filePath.isBlank()) { + return null; + } + // 2. Try resolving filesystem location via Workspace without taking workspace + // locks + java.nio.file.Path nioPath = null; + try { + org.eclipse.core.runtime.IPath eclipsePath = new org.eclipse.core.runtime.Path(filePath); + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(eclipsePath); + + if (file != null && file.getLocation() != null) { + // Get direct OS filesystem path from IFile (prevents blocking + // file.getContents() lock) + nioPath = file.getLocation().toFile().toPath(); + } + } catch (Exception e) { + // Fallback if path isn't a valid workspace path + } + if (nioPath == null) { + try { + nioPath = java.nio.file.Paths.get(filePath); + } catch (Exception e) { + return null; + } + } + // 3. Perform standard Java NIO read on physical path (Interrupt-safe) + try { + if (java.nio.file.Files.exists(nioPath) && java.nio.file.Files.isRegularFile(nioPath)) { + return java.nio.file.Files.readString(nioPath, java.nio.charset.StandardCharsets.UTF_8); + } + } catch (java.io.IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } catch (Exception e) { + if (e instanceof InterruptedException || e.getCause() instanceof InterruptedException) { + // Restore interrupted flag without failing the application + Thread.currentThread().interrupt(); + CxLogger.warning(LOG_TAG + " File reading interrupted for: " + filePath); + } else { + CxLogger.warning(LOG_TAG + " Unexpected error reading file: " + e.getMessage()); + } + } + return null; + } + + /** + * Run ASCA scan with synchronized temp file management. Ensures temp files are + * properly created and cleaned up. + */ + private Object runAscaScan(String filePath, String fileContent) { + synchronized (SCAN_LOCK) { + String tempFilePath = saveTempFile(Paths.get(filePath).getFileName().toString(), fileContent); + if (tempFilePath == null) { + CxLogger.warning(LOG_TAG + " Failed to create temporary file"); + return null; + } + + try { + CxLogger.info(LOG_TAG + " Starting ASCA scan: " + filePath); + String ignoreFilePath = getIgnoreFilePath(); + Object scanResult = executeAscaScanner(tempFilePath, ignoreFilePath); + CxLogger.info(LOG_TAG + " ASCA scan completed"); + return scanResult; + } finally { + deleteFile(tempFilePath); + } + } + } + + /** + * Execute ASCA scan using CxWrapperFactory. + */ + private Object executeAscaScanner(String filePath, String ignoreFilePath) { + try { + return scanAscaFile(filePath, true, "Eclipse", ignoreFilePath); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " ASCA scan error: " + e.getMessage(), e); + return null; + } + } + + /** + * Get ignore file path for ASCA scanning. + * Returns empty string by default - can be extended to read from .checkmarxIgnored file. + */ + private String getIgnoreFilePath() { + return ""; + } + + /** + * Get secure temporary directory with validation. Prevents directory traversal + * attacks. + */ + private Path getSecureTempDirectory() throws SecurityException { + try { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.trim().isEmpty()) { + throw new SecurityException("System temp directory not available"); + } + + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + + if (!Files.exists(baseTempDir) || !Files.isDirectory(baseTempDir)) { + throw new SecurityException("System temp directory not valid: " + baseTempDir); + } + + Path ascaTempDir = baseTempDir.resolve(ASCA_DIR).normalize(); + + // Security check: ensure ASCA dir is within system temp + if (!ascaTempDir.startsWith(baseTempDir)) { + throw new SecurityException("ASCA temp directory outside system temp"); + } + + return ascaTempDir; + + } catch (Exception e) { + throw new SecurityException("Failed to create secure temp directory", e); + } + } + + private String saveTempFile(String fileName, String fileContent) { + try { + // Get secure temp directory + Path tempDir = getSecureTempDirectory(); + createTempFolder(tempDir); + + // Sanitize fileName to prevent directory traversal attacks + String sanitizedFileName = sanitizeFileName(fileName); + + // Create secure path with normalization + Path tempFilePath = tempDir.resolve(sanitizedFileName).normalize(); + + // Security check: ensure the resolved path is still within the temp directory + if (!tempFilePath.startsWith(tempDir)) { + return null; + } + + Files.write(tempFilePath, fileContent.getBytes()); + return tempFilePath.toAbsolutePath().toString(); + } catch (SecurityException e) { + return null; + } catch (IOException e) { + return null; + } + } + + + /** + * Sanitize file name to prevent directory traversal attacks. + */ + private String sanitizeFileName(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + return "temp_asca.tmp"; + } + + // Remove path separators and dangerous characters + String sanitized = fileName.replaceAll("[/\\\\:*?\"<>|]", "_").replaceAll("\\.\\.+", ".") // Replace multiple + // dots + .trim(); + + if (sanitized.isEmpty() || sanitized.equals(".") || sanitized.equals("..")) { + sanitized = "temp_asca.tmp"; + } + + // Limit length for filesystem compatibility + if (sanitized.length() > 200) { + String extension = ""; + int lastDot = sanitized.lastIndexOf('.'); + if (lastDot > 0) { + extension = sanitized.substring(lastDot); + sanitized = sanitized.substring(0, Math.min(200 - extension.length(), lastDot)); + } else { + sanitized = sanitized.substring(0, 200); + } + sanitized = sanitized + extension; + } + + return sanitized; + } + + /** + * Delete temporary file with security checks. + */ + private void deleteFile(String filePath) { + if (filePath == null || filePath.trim().isEmpty()) { + return; + } + + try { + Path path = Paths.get(filePath).toAbsolutePath().normalize(); + Path tempDir = getSecureTempDirectory(); + + // Security check: only delete files in temp directory + if (!path.startsWith(tempDir)) { + CxLogger.warning(LOG_TAG + " Security violation: file outside temp: " + filePath); + return; + } + + Files.deleteIfExists(path); + CxLogger.info(LOG_TAG + " Temporary file deleted: " + path); + + } catch (SecurityException e) { + CxLogger.error(LOG_TAG + " Security error deleting file: " + e.getMessage(), e); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to delete temp file: " + filePath); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Unexpected error deleting temp file: " + e.getMessage()); + } + } + + + private com.checkmarx.ast.asca.ScanResult scanAscaFile(String path, boolean ascaLatestVersion, String agent, + String ignoreFilePath) throws IOException, CxException, InterruptedException { + com.checkmarx.ast.asca.ScanResult scanResult = null; + try { + scanResult = CxWrapperFactory.build().ScanAsca(path, ascaLatestVersion, agent, null); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (CxException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + return scanResult; + } + +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java new file mode 100644 index 00000000..6c95c32f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java @@ -0,0 +1,175 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeImage; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeVulnerability; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adaptor for Container image scan results in Eclipse. + * + * Converts typed container vulnerability data (ContainersRealtimeResults) into + * standardized ScanIssue, Vulnerability, and Location objects. + */ +public class ContainerScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[CONTAINER-ADAPTOR]"; + + private static final String MALICIOUS_RISK_CONTAINER = "Container image contains malicious risk dependencies or configuration."; + private static final String CRITICAL_RISK_CONTAINER = "Container image contains critical severity security vulnerabilities."; + private static final String HIGH_RISK_CONTAINER = "Container image contains high severity security vulnerabilities."; + private static final String MEDIUM_RISK_CONTAINER = "Container image contains medium severity security vulnerabilities."; + private static final String LOW_RISK_CONTAINER = "Container image contains low severity security vulnerabilities."; + + private final ContainersRealtimeResults containersRealtimeResults; + private final String fileType; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of ContainerScanResultAdaptor with typed Container real-time results. + * + * @param containersRealtimeResults the container real-time scan results from AST SDK + * @param fileType the file extension/type (e.g., "dockerfile") + * @param filePath the project-relative or absolute file path + */ + public ContainerScanResultAdaptor(ContainersRealtimeResults containersRealtimeResults, String fileType, String filePath) { + this.containersRealtimeResults = containersRealtimeResults; + this.fileType = fileType; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public ContainersRealtimeResults getResults() { + return containersRealtimeResults; + } + + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Processes images obtained from the scan results and converts them into standardized scan issues. + */ + public List buildIssues() { + List images = Objects.nonNull(getResults()) ? getResults().getImages() : null; + if (Objects.isNull(images) || images.isEmpty()) { + return Collections.emptyList(); + } + return images.stream() + .map(this::createScanIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Creates a ScanIssue object based on the provided ContainersRealtimeImage. + */ + private ScanIssue createScanIssue(ContainersRealtimeImage containersImageObj) { + try { + ScanIssue scanIssue = new ScanIssue(); + scanIssue.setScanEngine(ScanEngine.CONTAINERS); + scanIssue.setTitle(containersImageObj.getImageName()); + scanIssue.setImageTag(containersImageObj.getImageTag()); + scanIssue.setSeverity(DevAssistUtils.normalizeSeverity(containersImageObj.getStatus())); + scanIssue.setFileType(this.fileType); + scanIssue.setFilePath(this.filePath); + + if (Objects.nonNull(containersImageObj.getLocations()) && !containersImageObj.getLocations().isEmpty()) { + containersImageObj.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + if (Objects.nonNull(containersImageObj.getVulnerabilities()) && !containersImageObj.getVulnerabilities().isEmpty()) { + containersImageObj.getVulnerabilities().forEach(vulnerability -> + scanIssue.getVulnerabilities().add(createVulnerability(vulnerability))); + } + + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + + int line = !scanIssue.getLocations().isEmpty() ? scanIssue.getLocations().get(0).getLine() : 1; + scanIssue.setProblematicLineNumber(line); + + return scanIssue; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating scan issue for image " + containersImageObj.getImageName() + ": " + e.getMessage()); + return null; + } + } + + /** + * Creates a Vulnerability instance based on the provided ContainersRealtimeVulnerability. + */ + private Vulnerability createVulnerability(ContainersRealtimeVulnerability vulnerabilityObj) { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setCve(vulnerabilityObj.getCve()); + vulnerability.setDescription(this.getDescription(vulnerabilityObj.getSeverity())); + vulnerability.setSeverity(DevAssistUtils.normalizeSeverity(vulnerabilityObj.getSeverity())); + return vulnerability; + } + + /** + * Maps severity string into standard risk description text for container vulnerabilities. + */ + private String getDescription(String severity) { + if (Objects.isNull(severity) || severity.isEmpty()) { + return severity; + } + String normalized = severity.toUpperCase(); + switch (normalized) { + case "MALICIOUS": + return MALICIOUS_RISK_CONTAINER; + case "CRITICAL": + return CRITICAL_RISK_CONTAINER; + case "HIGH": + return HIGH_RISK_CONTAINER; + case "MEDIUM": + return MEDIUM_RISK_CONTAINER; + case "LOW": + return LOW_RISK_CONTAINER; + default: + return severity; + } + } + + /** + * Creates a Location object based on the provided RealtimeLocation. + * Note: Adjusts zero-based line numbers from scan results to one-based line numbers. + */ + private Location createLocation(RealtimeLocation location) { + int line = getLine(location); + int startIndex = location.getStartIndex(); + int endIndex = location.getEndIndex(); + return new Location(line, startIndex, endIndex); + } + + /** + * Retrieves the line number from the given RealtimeLocation object and increments it by 1. + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue. + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() : 0; + return DevAssistUtils.generateUniqueId(line, scanIssue.getTitle(), scanIssue.getImageTag()); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java new file mode 100644 index 00000000..81f67eb1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java @@ -0,0 +1,109 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import java.util.Objects; + +/** + * Container Scanner Command that manages the lifecycle of container realtime scanning in Eclipse. + * Coordinates execution, file eligibility validation, and disposal for a given workspace project. + * Extends BaseScannerCommand for consistent registration lifecycle. + */ +public class ContainerScannerCommand extends BaseScannerCommand { + + private static final String LOG_TAG = "[CONTAINER-COMMAND]"; + + private final ContainerScannerService containerScannerService; + private boolean isInitialized = false; + + /** + * Main constructor for initializing the command with a project. + * + * @param project the Eclipse project instance + */ + public ContainerScannerCommand(IProject project) { + this(project, new ContainerScannerService(project)); + } + + /** + * Dependency injection constructor (useful for unit testing or custom service setup). + * + * @param project the Eclipse project instance + * @param containerScannerService custom or pre-configured scanner service + */ + public ContainerScannerCommand(IProject project, ContainerScannerService containerScannerService) { + super(project, ContainerScannerService.createConfig()); + this.containerScannerService = containerScannerService; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + /** + * Initializes the scanner, invoked when scanner is registered. + */ + @Override + public void initializeScanner() { + if (!isInitialized) { + this.isInitialized = true; + String projectName = Objects.nonNull(project) ? project.getName() : "Unknown"; + CxLogger.info(LOG_TAG + " Container Scanner Command initialized for project: " + projectName); + } + } + + /** + * Evaluates whether the specified file path is eligible for a Container scan + * (Dockerfiles, Docker Compose, or Helm charts). + * + * @param filePath project-relative or absolute file path + * @return true if the file should be scanned, false otherwise + */ + public boolean shouldScan(String filePath) { + return containerScannerService.shouldScanFile(filePath); + } + + /** + * Triggers a Container Realtime scan for the specified file path and active document. + * + * @param filePath absolute path to the file being scanned + * @param document the open Eclipse IDocument buffer (or null if scanning directly from disk) + * @return strongly typed ScanResult containing ContainersRealtimeResults and converted ScanIssues + */ + public ScanResult scan(String filePath, IDocument document) { + if (!shouldScan(filePath)) { + return null; + } + return containerScannerService.scan(filePath, document, project); + } + + /** + * Returns the underlying ContainerScannerService instance. + * + * @return the active ContainerScannerService + */ + public ContainerScannerService getScannerService() { + return containerScannerService; + } + + /** + * Disposes underlying resources and cleans up temporary structures. + * Automatically called when the project or plugin context is closed/unloaded. + */ + @Override + public void dispose() { + try { + if (containerScannerService != null) { + containerScannerService.close(); + } + this.isInitialized = false; + String projectName = Objects.nonNull(project) ? project.getName() : "Unknown"; + CxLogger.info(LOG_TAG + " Container Scanner Command disposed for project: " + projectName); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing Container Scanner Command: " + e.getMessage()); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java new file mode 100644 index 00000000..6359ec88 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -0,0 +1,296 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Container image scanner service for Eclipse. + * + * Handles file detection (Docker, Docker Compose, Helm), secure temporary folder management, + * and direct invocation of Checkmarx Container Realtime scanning via CxWrapperFactory. + */ +public class ContainerScannerService extends BaseScannerService { + + private static final String LOG_TAG = "[CONTAINER-SERVICE]"; + private static final String CONTAINER_DIR = "CxContainer"; + private static final Object SCAN_LOCK = new Object(); + + private static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile*", + "**/*.containerfile", + "**/*.image", + "**/docker-compose*.yml", + "**/docker-compose*.yaml" + ); + + private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of( + "chart.yaml", + "chart.yml", + "values.yaml", + "values.yml" + ); + + private String fileType; + + public ContainerScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default Container scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.CONTAINERS.name()) + .configSection(DevAssistConstants.CONTAINER_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_CONTAINER_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_CONTAINER_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); + } + + /** + * Checks whether the supplied file path matches container file patterns (Dockerfile, Docker Compose, etc.). + */ + private boolean isContainersFilePatternMatching(String filePath) { + String lowerPath = filePath.toLowerCase(); + List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { + if (DevAssistUtils.isDockerComposeFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKER_COMPOSE; + } else if (DevAssistUtils.isDockerFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKERFILE; + } + return true; + } + } + return false; + } + + /** + * Checks whether the supplied file path is part of a Helm chart. + */ + public boolean isHelmFile(String filePath) { + if (filePath == null) { + return false; + } + String lowerPath = filePath.toLowerCase(); + if (DevAssistUtils.isYamlFile(lowerPath)) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { + return false; + } + if (lowerPath.contains("/helm/")) { + this.fileType = DevAssistUtils.HELM; + return true; + } + } + return false; + } + + /** + * Primary scan method. Reads content, creates isolated temporary directory structure, + * executes the container realtime scan, and updates ignored issues. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + synchronized (SCAN_LOCK) { + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); + return null; + } + + Path tempBaseDir = getSecureTempDirectory(); + Path tempSubFolder = null; + Path tempFilePath = null; + + try { + String fileName = Paths.get(filePath).getFileName().toString(); + String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; + String folderName = prefix + generateFileHash(filePath); + + tempSubFolder = tempBaseDir.resolve(folderName).normalize(); + createTempFolder(tempSubFolder); + + tempFilePath = tempSubFolder.resolve(fileName).normalize(); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + + CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); +// String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj != null ? proj : this.project); + + ContainersRealtimeResults scanResults = null; + try { + scanResults = CxWrapperFactory.build().containersRealtimeScan(tempFilePath.toString(), ""); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, filePath); + + return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); + + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); + } finally { + if (Objects.nonNull(tempSubFolder)) { + deleteTempFolder(tempSubFolder); + } + } + } + return null; + } + + /** + * Re-runs scan without ignore settings to calculate line updates for ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// IgnoreManager ignoreManager = new IgnoreManager(proj); +// if (ignoreManager.hasIgnoredEntries(ScanEngine.CONTAINERS)) { +// CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); +// ContainersRealtimeResults fullScanResults = CxWrapperFactory.build() +// .containersRealtimeScan(tempFilePath, ""); +// +// if (fullScanResults != null) { +// ContainerScanResultAdaptor fullScanResultAdaptor = new ContainerScanResultAdaptor(fullScanResults, this.fileType, filePath); +// ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); +// } +// } +// } catch (Exception e) { +// CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored file line numbers: " + e.getMessage()); +// } + } + + /** + * Reads file content from Eclipse IDocument buffer or disk filesystem. + */ + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + /** + * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary directory names. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.isBlank()) { + tempOSPath = System.getProperty("user.home"); + } + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + return baseTempDir.resolve(CONTAINER_DIR).normalize(); + } + + protected void createTempFolder(Path tempDir) { + if (!Files.exists(tempDir)) { + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } + } + } + + protected void deleteTempFolder(Path path) { + if (path == null || !Files.exists(path)) { + return; + } + try { + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + CxLogger.info(LOG_TAG + " Temporary folder deleted: " + path.toAbsolutePath()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to delete temporary directory: " + e.getMessage()); + } + } + + /** + * Compatibility method matching ScannerService interface. + */ + @Override + public ScanResult scan(String filePath) { + if (!shouldScanFile(filePath)) { + return null; + } + IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument, project); + } + + @Override + public void close() throws Exception { + // No persistent connections to close + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java new file mode 100644 index 00000000..05baecfb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -0,0 +1,254 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Adapter class for handling IaC scan results and converting them into a standardized format. + * + * This class wraps an IaC {@link IacRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on IaC misconfigurations detected in the files. + * + * Features: + * - Groups multiple misconfigurations on the same line + * - Sorts misconfigurations by severity precedence + * - Generates proper unique IDs + * - Tracks location information + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class IacScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[IAC-ADAPTOR]"; + private static final String MULTIPLE_ISSUES_SUFFIX = " IaC misconfigurations"; + + private final IacRealtimeResults iacRealtimeResults; + private final String filePath; + private final List scanIssues; + + public IacScanResultAdaptor(IacRealtimeResults iacRealtimeResults, String filePath) { + this.iacRealtimeResults = iacRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public IacRealtimeResults getResults() { + return iacRealtimeResults; + } + + @Override + public List getIssues() { + return scanIssues; + } + + private List buildIssues() { + if (iacRealtimeResults == null || iacRealtimeResults.getResults() == null) { + CxLogger.info(LOG_TAG + " No scan results available"); + return Collections.emptyList(); + } + + List issues = iacRealtimeResults.getResults(); + if (issues.isEmpty()) { + return Collections.emptyList(); + } + + // Group issues by line number, then sort by severity precedence + Map> groupedIssues = issues.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy( + issue -> { + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + return issue.getLocations().get(0).getLine(); + } + return 1; + }, + Collectors.collectingAndThen(Collectors.toList(), issuesList -> { + issuesList.sort(Comparator.comparingInt(issue -> + getSeverityPrecedence(issue.getSeverity()))); + return issuesList; + }) + )); + + List scanIssues = groupedIssues.values().stream() + .map(this::createScanIssueForGroup) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + scanIssues.size() + " grouped scan issues for file: " + filePath); + return scanIssues; + } + + private ScanIssue createScanIssueForGroup(List iacIssues) { + if (iacIssues == null || iacIssues.isEmpty()) { + return null; + } + + try { + ScanIssue scanIssue = getScanIssue(iacIssues); + + // Add vulnerabilities from all issues in the group + for (int i = 0; i < iacIssues.size(); i++) { + IacRealtimeResults.Issue iacIssue = iacIssues.get(i); + String vulnerabilityId = (i == 0) ? scanIssue.getScanIssueId() : null; + Vulnerability vuln = createVulnerability(iacIssue, vulnerabilityId); + scanIssue.getVulnerabilities().add(vuln); + } + + // Update title based on actual number of vulnerabilities + updateScanIssueTitleAndLocation(scanIssue, iacIssues); + + CxLogger.info(LOG_TAG + " Created ScanIssue with " + scanIssue.getVulnerabilities().size() + + " vulnerabilities on line " + scanIssue.getProblematicLineNumber()); + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert issues group to ScanIssue: " + e.getMessage()); + return null; + } + } + + private ScanIssue getScanIssue(List iacIssues) { + ScanIssue scanIssue = new ScanIssue(); + IacRealtimeResults.Issue firstIssue = iacIssues.get(0); + + int firstLine = 1; + if (firstIssue.getLocations() != null && !firstIssue.getLocations().isEmpty()) { + firstLine = firstIssue.getLocations().get(0).getLine(); + } + + // Set title based on whether there are multiple issues on the same line + String title; + if (iacIssues.size() > 1) { + title = iacIssues.size() + MULTIPLE_ISSUES_SUFFIX; + } else { + title = firstIssue.getTitle(); + } + + scanIssue.setTitle(title); + scanIssue.setDescription(firstIssue.getDescription()); + scanIssue.setSeverity(mapSeverity(firstIssue.getSeverity())); + scanIssue.setFilePath(filePath); + scanIssue.setScanEngine(ScanEngine.IAC); + scanIssue.setProblematicLineNumber(firstLine); + + String scanIssueId = generateUniqueId(firstIssue, firstLine); + scanIssue.setScanIssueId(scanIssueId); + + return scanIssue; + } + + private Vulnerability createVulnerability(IacRealtimeResults.Issue iacIssue, String overrideId) { + Vulnerability vulnerability = new Vulnerability(); + + int firstLine = 1; + if (iacIssue.getLocations() != null && !iacIssue.getLocations().isEmpty()) { + firstLine = iacIssue.getLocations().get(0).getLine(); + } + + String vulnerabilityId = generateUniqueId(iacIssue, firstLine); + if (overrideId != null && !overrideId.isBlank()) { + vulnerabilityId = overrideId; + } + + vulnerability.setVulnerabilityId(vulnerabilityId); + vulnerability.setTitle(iacIssue.getTitle()); + vulnerability.setDescription(iacIssue.getDescription()); + vulnerability.setSeverity(mapSeverity(iacIssue.getSeverity())); + + CxLogger.info(LOG_TAG + " Created vulnerability '" + iacIssue.getTitle() + + "' with vulnerabilityId '" + vulnerabilityId + "'"); + + return vulnerability; + } + + private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List iacIssues) { + // Update title based on actual number of vulnerabilities + if (scanIssue.getVulnerabilities().size() == 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().get(0).getTitle()); + } else if (scanIssue.getVulnerabilities().size() > 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + MULTIPLE_ISSUES_SUFFIX); + } + + // Add location information from issues + for (IacRealtimeResults.Issue iacIssue : iacIssues) { + if (iacIssue.getLocations() != null) { + for (RealtimeLocation loc : iacIssue.getLocations()) { + Location location = new Location(); + location.setLine(loc.getLine()+1); + location.setStartIndex(loc.getStartIndex()); + location.setEndIndex(loc.getEndIndex()); + scanIssue.getLocations().add(location); + } + } + } + + // Ensure at least one location + if (scanIssue.getLocations().isEmpty()) { + Location location = new Location(); + location.setLine(scanIssue.getProblematicLineNumber()); + scanIssue.getLocations().add(location); + } + } + + private String mapSeverity(String severity) { + if (severity == null) { + return "Medium"; + } + + switch (severity.toLowerCase()) { + case "critical": + return "Critical"; + case "high": + return "High"; + case "medium": + return "Medium"; + case "low": + return "Low"; + case "info": + return "Low"; + default: + return "Medium"; + } + } + + private int getSeverityPrecedence(String severity) { + if (severity == null) { + return 3; + } + + switch (severity.toLowerCase()) { + case "critical": + return 5; + case "high": + return 4; + case "medium": + return 3; + case "low": + return 2; + case "info": + return 1; + default: + return 3; + } + } + + private String generateUniqueId(IacRealtimeResults.Issue iacIssue, int line) { + if (iacIssue != null) { + return DevAssistUtils.generateUniqueId( + line, + iacIssue.getSimilarityId() + iacIssue.getTitle(), + filePath); + } + return ScanEngine.IAC.name(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java new file mode 100644 index 00000000..0644bd10 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java @@ -0,0 +1,77 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * Command for coordinating IaC scanner operations. + * + * Manages the lifecycle of IaC realtime scanning in Eclipse, integrating with + * the scanner registry system to handle enabling/disabling of IaC scanning. + * Extends BaseScannerCommand for consistent registration lifecycle. + */ +public class IacScannerCommand extends BaseScannerCommand { + + private static final String LOG_TAG = "[IAC-COMMAND]"; + + private final IacScannerService scannerService; + + public IacScannerCommand(IProject project, IacScannerService scannerService) { + super(project, IacScannerService.createConfig()); + this.scannerService = scannerService; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + public IacScannerCommand(IProject project) { + this(project, new IacScannerService(project)); + } + + /** + * Initializes the scanner, invoked when scanner is registered. + * IaC scans are triggered on demand via editor file changes rather than bulk project scans. + */ + @Override + public void initializeScanner() { + CxLogger.info(LOG_TAG + " Initialized for project: " + project.getName()); + } + + /** + * Determines whether a file path should be scanned by the IaC scanner. + * + * @param filePath path to evaluate + * @return {@code true} if the file is an IaC file eligible for scanning + */ + public boolean shouldScan(String filePath) { + return scannerService.shouldScanFile(filePath); + } + + /** + * Executes an IaC scan on a specific file given its document content. + * + * @param filePath path to the file being scanned + * @param document editor document content + * @return ScanResult containing issues found, or null + */ + public ScanResult scan(String filePath, IDocument document) { + return scannerService.scan(filePath, document, project); + } + + /** + * Disposes the scanner and releases associated resources. + * Triggered when project is closed or scanner is unregistered. + */ + @Override + public void dispose() { + try { + scannerService.close(); + CxLogger.info(LOG_TAG + " Disposed for project: " + project.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java new file mode 100644 index 00000000..93934a2d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -0,0 +1,322 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime IaC scanner service for Eclipse. + * + * Manages temporary folder creation, file hash generation, type extraction + * (Terraform, CloudFormation, Kubernetes, Dockerfile, etc.), execution of + * Checkmarx IaC real-time scans, and updating ignored issue tracking data. + */ +public class IacScannerService extends BaseScannerService { + + private static final String LOG_TAG = "[IAC-SERVICE]"; + private static final String IAC_DIR = "CxIaC"; + private static final String DOCKERFILE = "dockerfile"; + private static final Object SCAN_LOCK = new Object(); + + // Supported glob patterns for IaC files + private static final List IAC_SUPPORTED_PATTERNS = List.of( + "*.tf", "*.tf.json", + "*.yaml", "*.yml", + "*.json", + "Dockerfile", "Dockerfile.*", "*.dockerfile", "dockerfile", "dockerfile.*" + ); + + // Supported extensions for IaC files + private static final Set IAC_FILE_EXTENSIONS = Set.of( + "tf", "tf.json", "yaml", "yml", "json", "dockerfile" + ); + + private String fileType; + + public IacScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default IaC scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.IAC.name()) + .configSection(DevAssistConstants.IAC_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_IAC_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.IAC_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.IAC_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_IAC_REALTIME_SCANNER) + .build(); + } + + /** + * Checks if the provided file path corresponds to a supported IaC file. + * Also detects and assigns the appropriate file type (e.g., dockerfile or extension). + */ + @Override + protected boolean isFileTypeSupported(String filePath) { + if (filePath == null || filePath.isBlank()) { + return false; + } + + String lowerPath = filePath.toLowerCase(); + List pathMatchers = IAC_SUPPORTED_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path.getFileName())) { + fileType = isDockerFile(lowerPath) ? DOCKERFILE : getFileExtension(filePath); + return true; + } + } + + String extension = getFileExtension(filePath); + if (extension == null) { + return false; + } + + fileType = extension.toLowerCase(); + return IAC_FILE_EXTENSIONS.contains(fileType); + } + + @Override + public void close() throws Exception { + // No resources to release + } + + /** + * Primary scan method. Converts editor/document contents to a temporary isolated file + * and executes the real-time IaC scan via CxWrapperFactory. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + Path tempFolderPath = getSecureTempDirectory(); + Pair saveResult = null; + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempFolderPath); + + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " No content found in file: " + filePath); + return null; + } + + saveResult = saveTempFiles(tempFolderPath, filePath, fileContent); + if (Objects.nonNull(saveResult)) { + String tempFilePath = saveResult.getLeft().toString(); + CxLogger.info(LOG_TAG + " Start IAC Realtime Scan On File: " + filePath); + + String containerTool = "docker"; +// String ignoreFilePath = getIgnoreFilePath(proj); + + IacRealtimeResults scanResults = null; + try { + scanResults = CxWrapperFactory.build() + .iacRealtimeScan(tempFilePath, containerTool, ""); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + if (scanResults == null) { + return null; + } + + IacScanResultAdaptor scanResultAdaptor = new IacScanResultAdaptor(scanResults, filePath); + + // Perform secondary scan to sync updated line numbers for ignored issues if needed +// updateIgnoredFileDataOnLatestResult(tempFilePath, proj, filePath); + + return scanResultAdaptor; + } + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Error executing IaC scanner for " + filePath + ": " + e.getMessage(), e); + } finally { + CxLogger.info(filePath); + if (Objects.nonNull(saveResult)) { + deleteTempFolder(saveResult.getRight()); + } + } + } + return null; + } + + /** + * Compatibility method matching ScannerService interface. + */ + @Override + public ScanResult scan(String filePath) { + if (!shouldScanFile(filePath)) { + return null; + } + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument != null ? liveDocument : new Document(), project); + } + + /** + * Performs a full scan without passing the ignore file to update line numbers of ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.debug(LOG_TAG + " IaC: Performing full scan without ignore file to update line numbers"); +// +// IacRealtimeResults fullScanResults = CxWrapperFactory.build() +// .iacRealtimeScan(tempFilePath, DevAssistUtils.getContainerTool(), ""); +// +// if (fullScanResults != null) { +// IacScanResultAdaptor fullScanResultAdaptor = new IacScanResultAdaptor(fullScanResults, fileType, filePath); +// // Hook for updating ignored line markers if IgnoreManager is active +// } +// } +// } catch (IOException | CxException | InterruptedException e) { +// CxLogger.warning(LOG_TAG + " RTS-IaC: Exception occurred while performing full scan without ignore file: " + e.getMessage()); +// } + } + + /** + * Saves file content to an isolated subfolder inside the temporary directory using a hashed name. + */ + private Pair saveTempFiles(Path tempFolder, String filePath, String fileContent) throws IOException { + String fileName = Paths.get(filePath).getFileName().toString(); + Path tempSubFolder = tempFolder.resolve(fileName + "-" + generateFileHash(fileName)); + return createSubFolderAndSaveFile(tempSubFolder, fileName, fileContent); + } + + /** + * Creates a target subfolder and writes the file content. + */ + private Pair createSubFolderAndSaveFile(Path tempSubFolder, String fileName, String fileContent) throws IOException { + createTempFolder(tempSubFolder); + Path fullTargetPath = tempSubFolder.resolve(fileName); + Files.writeString(fullTargetPath, fileContent, StandardCharsets.UTF_8); + return Pair.of(fullTargetPath, tempSubFolder); + } + + /** + * Generates a 16-character SHA-256 hash derived from the relative file path and timestamp. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + return Paths.get(tempOSPath, IAC_DIR).toAbsolutePath().normalize(); + } + + protected void createTempFolder(Path tempDir) { + if (!Files.exists(tempDir)) { + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } + } + } + + protected void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + private boolean isDockerFile(String filePath) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + return fileName.contains("dockerfile"); + } + + private String getFileExtension(String filePath) { + if (filePath == null) { + return null; + } + int lastDot = filePath.lastIndexOf('.'); + if (lastDot > 0 && lastDot < filePath.length() - 1) { + return filePath.substring(lastDot + 1).toLowerCase(); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java new file mode 100644 index 00000000..17df8fab --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java @@ -0,0 +1,196 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.ast.ossrealtime.OssRealtimeScanPackage; +import com.checkmarx.ast.ossrealtime.OssRealtimeVulnerability; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adaptor class for handling OSS scan results and converting them into a standardized format + * using the {@link ScanResult} interface. + * + * This class wraps an {@link OssRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on vulnerabilities detected in the packages. + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class OssScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[OSS-ADAPTOR]"; + + private final OssRealtimeResults ossRealtimeResults; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of {@code OssScanResultAdaptor} with the specified OSS real-time results. + * + * @param ossRealtimeResults the OSS real-time scan results to be wrapped by this adapter + * @param filePath the path of the file being scanned + */ + public OssScanResultAdaptor(OssRealtimeResults ossRealtimeResults, String filePath) { + this.ossRealtimeResults = ossRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + /** + * Retrieves the raw OSS real-time scan results wrapped by this adapter. + * + * @return an {@link OssRealtimeResults} instance containing the results of the OSS scan + */ + @Override + public OssRealtimeResults getResults() { + return ossRealtimeResults; + } + + /** + * Retrieves a list of scan issues discovered in the OSS real-time scan. + * + * @return a list of {@link ScanIssue} objects representing findings, or an empty list if none + */ + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Builds a list of ScanIssue objects from the OSS scan results. + * Processes packages obtained from scan results into standardized ScanIssue items. + * + * @return a list of ScanIssue objects + */ + private List buildIssues() { + List packages = Objects.nonNull(getResults()) ? getResults().getPackages() : null; + if (Objects.isNull(packages) || packages.isEmpty()) { + CxLogger.info(LOG_TAG + " No scan results or packages available"); + return Collections.emptyList(); + } + + List issues = packages.stream() + .map(this::createScanIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + issues.size() + " OSS scan issues for file: " + filePath); + return issues; + } + + /** + * Creates a {@link ScanIssue} object based on the provided {@link OssRealtimeScanPackage}. + * + * @param packageObj the package object containing scan findings + * @return a structured {@link ScanIssue} instance + */ + private ScanIssue createScanIssue(OssRealtimeScanPackage packageObj) { + if (packageObj == null) { + return null; + } + + try { + ScanIssue scanIssue = new ScanIssue(); + + scanIssue.setPackageManager(packageObj.getPackageManager()); + scanIssue.setTitle(packageObj.getPackageName()); + scanIssue.setPackageVersion(packageObj.getPackageVersion()); + scanIssue.setScanEngine(ScanEngine.OSS); + scanIssue.setSeverity(DevAssistUtils.normalizeSeverity(packageObj.getStatus())); + scanIssue.setFilePath(this.filePath); + + // Process location information + if (Objects.nonNull(packageObj.getLocations()) && !packageObj.getLocations().isEmpty()) { + packageObj.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + // Process vulnerabilities + if (Objects.nonNull(packageObj.getVulnerabilities()) && !packageObj.getVulnerabilities().isEmpty()) { + packageObj.getVulnerabilities().forEach(vulnerability -> + scanIssue.getVulnerabilities().add(createVulnerability(vulnerability))); + } + + // Set primary problem line based on first location (if available) + int primaryLine = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() + : 1; + scanIssue.setProblematicLineNumber(primaryLine); + + // Generate unique ID based on line, package manager + title, and version + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert package to ScanIssue: " + e.getMessage()); + return null; + } + } + + /** + * Creates a {@link Vulnerability} instance based on the provided {@link OssRealtimeVulnerability}. + * + * @param vulnerabilityObj the OSS vulnerability object + * @return a standardized {@link Vulnerability} object + */ + private Vulnerability createVulnerability(OssRealtimeVulnerability vulnerabilityObj) { + Vulnerability vulnerability = new Vulnerability(); + + vulnerability.setCve(vulnerabilityObj.getCve()); + vulnerability.setTitle(vulnerabilityObj.getCve()); + vulnerability.setDescription(vulnerabilityObj.getDescription()); + vulnerability.setSeverity(DevAssistUtils.normalizeSeverity(vulnerabilityObj.getSeverity())); + vulnerability.setFixVersion(vulnerabilityObj.getFixVersion()); + + return vulnerability; + } + + /** + * Creates a {@link Location} object based on the provided {@link RealtimeLocation}. + * + * @param location the real-time location details + * @return a new {@link Location} instance with 1-based line indexing + */ + private Location createLocation(RealtimeLocation location) { + return new Location(getLine(location), location.getStartIndex(), location.getEndIndex()); + } + + /** + * Adjusts zero-based line numbers from OSS scanner to 1-based line numbers. + * + * @param location the real-time location + * @return 1-based line number + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue using line, package identifier, and version. + * + * @param scanIssue the scan issue + * @return unique string identifier + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() + : 0; + + return DevAssistUtils.generateUniqueId( + line, + scanIssue.getPackageManager() + scanIssue.getTitle(), + scanIssue.getPackageVersion() + ); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java new file mode 100644 index 00000000..b9fb39c1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -0,0 +1,178 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.runtime.CoreException; +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.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Command for coordinating OSS scanner operations in Eclipse. + * + * Manages the lifecycle and initialization of OSS scanning: + * - Extends BaseScannerCommand for consistent registration lifecycle + * - Traverses project workspace files recursively upon initialization + * - Executes background job scans on supported manifest files + * - Publishes findings via ProblemHolderService + */ +public class OssScannerCommand extends BaseScannerCommand { + + private static final String LOG_TAG = "[OSS-COMMAND]"; + + public final OssScannerService ossScannerService; + + public OssScannerCommand(IProject project) { + super(project, OssScannerService.createConfig()); + this.ossScannerService = new OssScannerService(project); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + /** + * Initializes the scanner, invoked when scanner is registered. + * Launches a background Eclipse Job to scan all manifest files in the project workspace. + */ + @Override + public void initializeScanner() { + Job scanJob = new Job("Starting Checkmarx OSS Real-time Scan") { + @Override + protected IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Scanning manifest files in project: " + project.getName(), IProgressMonitor.UNKNOWN); + scanAllManifestFilesInFolder(monitor); + monitor.done(); + return Status.OK_STATUS; + } + }; + scanJob.schedule(); + } + + /** + * Scans all manifest files in the opened project workspace. + * Recursively iterates through project resources (excluding node_modules) + * and triggers an OSS real-time scan on each matching manifest file. + */ + private void scanAllManifestFilesInFolder(IProgressMonitor monitor) { + if (project == null || !project.isOpen()) { + return; + } + + List matchedFiles = new ArrayList<>(); + + List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + try { + // Recursively traverse project workspace files (equivalent to ProjectRootManager in JetBrains) + project.accept(new IResourceVisitor() { + @Override + public boolean visit(IResource resource) throws CoreException { + if (monitor.isCanceled()) { + return false; + } + + // Skip node_modules folder subtree entirely + if (resource.getType() == IResource.FOLDER && resource.getName().equals("node_modules")) { + return false; + } + + if (resource.getType() == IResource.FILE && resource.exists()) { + IFile file = (IFile) resource; + String path = file.getLocation() != null ? file.getLocation().toOSString() : file.getFullPath().toString(); + + for (PathMatcher matcher : pathMatchers) { + if (matcher.matches(Paths.get(path))) { + matchedFiles.add(file); + break; + } + } + } + return true; + } + }); + } catch (CoreException e) { + CxLogger.error(LOG_TAG + " Exception during workspace traversal for project " + project.getName() + ": " + e.getMessage(), e); + } + + // Execute scan on each discovered manifest file + for (IFile file : matchedFiles) { + if (monitor.isCanceled()) { + break; + } + + String uri = file.getLocation() != null ? file.getLocation().toOSString() : file.getFullPath().toString(); + try { + // Perform OSS scan using service + ScanResult ossRealtimeResults = ossScannerService.scanWithDocument(uri, new Document()); + + if (Objects.isNull(ossRealtimeResults)) { + CxLogger.warning(LOG_TAG + " Scan failed for manifest file: " + uri); + continue; + } + + // Add findings to problem markers + List issues = ossRealtimeResults.getIssues(); + ProblemHolderService.addToCxOneFindings(file, issues); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Scan failed for manifest file: " + uri + " with exception: " + e.getMessage()); + } + } + } + + /** + * Check if a file should be scanned by this command. + */ + public boolean shouldScan(String filePath) { + return ossScannerService.shouldScanFile(filePath); + } + + /** + * Execute scan on a file with document content. + */ + public ScanResult scan(String filePath, IDocument document) { + return ossScannerService.scanWithDocument(filePath, document); + } + + /** + * Execute scan on a file path directly. + */ + public ScanResult scan(String filePath) { + return ossScannerService.scan(filePath); + } + + /** + * Disposes the scanner and releases resources. + */ + @Override + public void dispose() { + try { + ossScannerService.close(); + CxLogger.info(LOG_TAG + " Disposed for project: " + project.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java new file mode 100644 index 00000000..0727a45d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -0,0 +1,310 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime OSS manifest scanner service for Eclipse that handles temporary file isolation, + * companion lock file resolution (e.g. package-lock.json), and invocation of the Checkmarx OSS engine. + * + * Adapted to mirror JetBrains scanner service features. + */ +public class OssScannerService extends BaseScannerService { + + private static final String LOG_TAG = "[OSS-SERVICE]"; + private static final String OSS_DIR = "CxOSS"; + private static final Object SCAN_LOCK = new Object(); + + public OssScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default OSS scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.OSS.name()) + .configSection(DevAssistConstants.OSS_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_OSS_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.OSS_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.OSS_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_OSS_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + if (filePath == null) { + return false; + } + + Path path = Paths.get(filePath); + List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path)) { + return true; + } + } + return false; + } + + @Override + public void close() throws Exception { + // No resources to close + } + + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scanWithDocument(filePath, liveDocument != null ? liveDocument : new Document()); + } + + /** + * Primary scan method - gets file content, isolates into temp folder with companion files, and executes scan. + */ + public ScanResult scanWithDocument(String filePath, IDocument document) { + if (!shouldScanFile(filePath)) { + return null; + } + + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " Could not read or empty file content: " + filePath); + return null; + } + + Path tempSubFolder = getTempSubFolderPathAsPath(filePath); + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempSubFolder); + + Optional mainTempPath = saveMainManifestFile(tempSubFolder, filePath, fileContent); + if (mainTempPath.isEmpty()) { + return null; + } + + // Copy companion lock file (e.g., package-lock.json) into temp folder if available + saveCompanionFile(tempSubFolder, filePath); + + CxLogger.info(LOG_TAG + " Starting Realtime OSS Scan on File: " + filePath); + + OssRealtimeResults scanResults = CxWrapperFactory.build().ossRealtimeScan(mainTempPath.get(), ""); + if (scanResults == null) { + return null; + } + + OssScanResultAdaptor scanResultAdaptor = new OssScanResultAdaptor(scanResults, filePath); + + return scanResultAdaptor; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Scan failed for file " + filePath + ": " + e.getMessage(), e); + return null; + } finally { + CxLogger.info(LOG_TAG + " Deleting temporary OSS folder"); + deleteTempFolder(tempSubFolder); + } + } + } + + /** + * Performs full scan without passing ignore file to update line numbers of ignored entries. + */ +// private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// // Extension point for ignore manager syncing when ignore files are active +// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); +// OssRealtimeResults fullScanResults = CxWrapperFactory.build().ossRealtimeScan(tempFilePath, ""); +// if (fullScanResults != null && fullScanResults.getPackages() != null) { +// OssScanResultAdaptor fullScanResultAdaptor = new OssScanResultAdaptor(fullScanResults, filePath); +// // Connects with ignore manager line number updater if implemented +// } +// } +// } catch (Exception e) { +// CxLogger.warning(LOG_TAG + " Exception occurred while performing full scan without ignore file: " + e.getMessage()); +// } +// } + + /** + * Persists the main manifest file into the temporary directory for scanning. + */ + private Optional saveMainManifestFile(Path tempSubFolder, String originalFilePath, String fileContent) { + try { + String fileName = Paths.get(originalFilePath).getFileName().toString(); + Path tempFilePath = tempSubFolder.resolve(fileName); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + return Optional.of(tempFilePath.toString()); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to write main manifest temp file: " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * Copies a companion lock file (e.g., package-lock.json) into the temporary directory + * when it exists alongside the scanned manifest. + */ + private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { + if (originalFilePath == null || originalFilePath.isEmpty() || tempFolderPath == null) { + return; + } + + Path originalPath = Paths.get(originalFilePath); + String parentFileName = originalPath.getFileName().toString(); + String companionFileName = getCompanionFileName(parentFileName); + + if (companionFileName.isEmpty()) { + return; + } + + Path parentPath = originalPath.getParent(); + if (parentPath == null) { + return; + } + + Path companionOriginalPath = parentPath.resolve(companionFileName); + if (!Files.exists(companionOriginalPath)) { + return; + } + + Path companionTempPath = tempFolderPath.resolve(companionFileName); + try { + Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); + CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); + } + } + + /** + * Infers companion lock file name based on manifest file name. + */ + private String getCompanionFileName(String fileName) { + if ("package.json".equalsIgnoreCase(fileName)) { + return "package-lock.json"; + } + if (fileName.toLowerCase().endsWith(".csproj")) { + return "package.lock.json"; + } + return ""; + } + + /** + * Resolves temporary sub-folder path allocated for the file scan. + */ + private Path getTempSubFolderPathAsPath(String filePath) { + String baseTempPath = System.getProperty("java.io.tmpdir"); + Path baseDir = Paths.get(baseTempPath).resolve(OSS_DIR); + String relativePath = Paths.get(filePath).getFileName().toString(); + return baseDir.resolve(toSafeTempFileName(relativePath, filePath)); + } + + /** + * Creates a deterministic, filesystem-safe file name for storing the manifest in the temp directory. + */ + private String toSafeTempFileName(String relativePath, String fullPath) { + String baseName = Paths.get(relativePath).getFileName().toString(); + String hash = generateFileHash(fullPath); + return baseName + "-" + hash; + } + + /** + * Generates a short hash based on the manifest path and current time to avoid collisions. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + protected void createTempFolder(Path tempDir) { + try { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temporary folder: " + e.getMessage()); + } + } + + protected void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java new file mode 100644 index 00000000..5f908056 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java @@ -0,0 +1,161 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adapter class for handling Secrets scan results and converting them into a standardized format + * using the {@link ScanResult} interface. + * This class wraps a {@link SecretsRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on secrets detected in the files. + */ +public class SecretsScanResultAdaptor implements ScanResult { + + private final SecretsRealtimeResults secretsRealtimeResults; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of {@code SecretsScanResultAdaptor} with the specified Secrets real-time results. + * This adapter allows conversion and processing of Secrets scan results into a standardized format. + * + * @param secretsRealtimeResults the Secrets real-time scan results to be wrapped by this adapter + * @param filePath the path of the scanned file + */ + public SecretsScanResultAdaptor(SecretsRealtimeResults secretsRealtimeResults, String filePath) { + this.secretsRealtimeResults = secretsRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + /** + * Retrieves the Secrets real-time scan results wrapped by this adapter. + * + * @return the Secrets scan results instance containing the results of the Secrets scan + */ + @Override + public SecretsRealtimeResults getResults() { + return secretsRealtimeResults; + } + + /** + * Retrieves a list of scan issues discovered in the Secrets real-time scan. + * + * @return a list of {@code ScanIssue} objects representing the secrets found during the scan + */ + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Retrieves a list of scan issues discovered in the Secrets real-time scan. + * This method processes the secrets obtained from the scan results, + * converts them into standardized scan issues, and returns the list. + * If no secrets are found, an empty list is returned. + * + * @return a list of {@code ScanIssue} objects representing the secrets found during the scan, + * or an empty list if no secrets are detected. + */ + public List buildIssues() { + if (Objects.isNull(getResults())) { + return Collections.emptyList(); + } + + List secrets = getResults().getSecrets(); + if (Objects.isNull(secrets) || secrets.isEmpty()) { + return Collections.emptyList(); + } + + return secrets.stream() + .map(this::createScanIssue) + .collect(Collectors.toList()); + } + + /** + * Creates a {@code ScanIssue} object based on the provided secret result. + * The method processes the secret details and converts them into a structured format to + * represent a scan issue. + * + * @param secret the secret result containing information about the detected secret, + * including its title, severity, description, and locations. + * @return a {@code ScanIssue} object encapsulating the details such as title, scan engine, + * severity, and secret locations derived from the provided secret result. + */ + private ScanIssue createScanIssue(SecretsRealtimeResults.Secret secret) { + ScanIssue scanIssue = new ScanIssue(); + + scanIssue.setTitle(secret.getTitle()); + scanIssue.setScanEngine(ScanEngine.SECRETS); + scanIssue.setSeverity(secret.getSeverity()); + scanIssue.setFilePath(this.filePath); + scanIssue.setDescription(secret.getDescription()); // Set description on ScanIssue for tooltip display + scanIssue.setSecretValue(secret.getSecretValue()); + + // Add locations if available + if (Objects.nonNull(secret.getLocations()) && !secret.getLocations().isEmpty()) { + secret.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + // Fallback location if none are provided by the engine + if (scanIssue.getLocations().isEmpty()) { + Location fallbackLocation = new Location(1, 0, 100); + scanIssue.getLocations().add(fallbackLocation); + } + + // Create vulnerability with secret details + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setTitle(secret.getTitle()); + vulnerability.setDescription(secret.getDescription()); + vulnerability.setSeverity(secret.getSeverity()); + + scanIssue.getVulnerabilities().add(vulnerability); + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + return scanIssue; + } + + /** + * Creates a {@code Location} object based on the provided location information. + * This method extracts the line, start index, and end index from the given + * location and constructs a new {@code Location} instance. + * + * @param location the location containing details such as line, + * start index, and end index for the location. + * @return a new {@code Location} instance with the appropriate line and indices. + */ + private Location createLocation(RealtimeLocation location) { + return new Location(getLine(location), location.getStartIndex(), location.getEndIndex()); + } + + /** + * Retrieves the line number from the given {@code RealtimeLocation} object, increments it by one, and returns the result. + * + * @param location the {@code RealtimeLocation} object containing the original line number + * @return the incremented line number based on the {@code RealtimeLocation}'s line value + * @apiNote Current Secrets scan result line numbers are zero-based, so this method adjusts them to be one-based. + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue. + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() : 0; + return DevAssistUtils.generateUniqueId(line, scanIssue.getTitle(), scanIssue.getDescription()); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java new file mode 100644 index 00000000..b5bb60bd --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java @@ -0,0 +1,53 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * Command for coordinating Secrets scanner operations. + * Extends BaseScannerCommand for consistent registration lifecycle. + */ +public class SecretsScannerCommand extends BaseScannerCommand { + + private static final String LOG_TAG = "[SECRETS-COMMAND]"; + + private final SecretsScannerService scannerService; + + public SecretsScannerCommand(IProject project) { + super(project, SecretsScannerService.createConfig()); + this.scannerService = new SecretsScannerService(project); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + /** + * Initializes the scanner, invoked when scanner is registered. + */ + @Override + public void initializeScanner() { + // Secrets scanning is triggered on demand via editor file changes + CxLogger.info(LOG_TAG + " Initialized for project: " + project.getName()); + } + + public boolean shouldScan(String filePath) { + return scannerService.shouldScanFile(filePath); + } + + public ScanResult scan(String filePath, IDocument document) { + return scannerService.scan(filePath, document, project); + } + + @Override + public void dispose() { + try { + scannerService.close(); + CxLogger.info(LOG_TAG + " Disposed for project: " + project.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); + } + super.dispose(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java new file mode 100644 index 00000000..5563f010 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -0,0 +1,300 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime Secrets scanner service for Eclipse. + * + * Manages temporary directory creation, file hashing, file exclusion filtering, + * execution of Checkmarx Secrets real-time scans via CxWrapperFactory, and updating + * line numbers for ignored secrets. + */ +public class SecretsScannerService extends BaseScannerService { + + private static final String LOG_TAG = "[SECRETS-SERVICE]"; + private static final String SECRETS_DIR = "CxSecrets"; + private static final Object SCAN_LOCK = new Object(); + + // Glob patterns for manifest files that should be excluded from Secrets scanning + private static final List MANIFEST_FILE_PATTERNS = List.of( + "package.json", "pom.xml", "go.mod", "requirements.txt", + "Gemfile", "Cargo.toml", "composer.json", "package-lock.json", "yarn.lock" + ); + + public SecretsScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default Secrets scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.SECRETS.name()) + .configSection(DevAssistConstants.SECRETS_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_SECRETS_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.SECRETS_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.SECRETS_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_SECRETS_REALTIME_SCANNER) + .build(); + } + + /** + * Determines whether a file should be excluded from Secrets scanning. + */ + private boolean isExcludedFileForSecretsScanning(String filePath) { + if (filePath == null || filePath.isBlank()) { + return true; + } + + Path path = Paths.get(filePath.toLowerCase()); + List manifestMatchers = MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + for (PathMatcher matcher : manifestMatchers) { + if (matcher.matches(path.getFileName())) { + return true; + } + } + + // Exclude Checkmarx ignore list files + String normalized = filePath.replace("\\", "/"); + return normalized.contains("/.checkmarxIgnored") || + normalized.contains("/.checkmarxIgnoredTempList"); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + return !isExcludedFileForSecretsScanning(filePath); + } + + @Override + public void close() throws Exception { + // No resources to release + } + + /** + * Primary scan method. Converts editor/document contents to an isolated temporary file + * and executes the real-time Secrets scan via CxWrapperFactory. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + Path tempSubFolder = getTempSubFolderPathAsPath(filePath); + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempSubFolder); + + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(filePath + " Secrets scanner: file content is empty or unreadable"); + return null; + } + + Optional tempFilePath = saveFileForScanning(tempSubFolder, filePath, fileContent); + if (tempFilePath.isEmpty()) { + CxLogger.warning(LOG_TAG + " Secrets scanner: failed to save file - " + filePath); + return null; + } + + CxLogger.info(LOG_TAG + " Starting scan: " + filePath); +// String ignoreFilePath = getIgnoreFilePath(proj); + + SecretsRealtimeResults scanResults = CxWrapperFactory.build() + .secretsRealtimeScan(tempFilePath.get(), ""); + + if (scanResults == null) { + CxLogger.warning(LOG_TAG + " Secrets scanner: no results returned - " + filePath); + return null; + } + + int secretCount = scanResults.getSecrets() != null ? scanResults.getSecrets().size() : 0; + CxLogger.info(LOG_TAG + " Scan completed: " + filePath + " (" + secretCount + " secrets found)"); + + SecretsScanResultAdaptor scanResultAdaptor = new SecretsScanResultAdaptor(scanResults, filePath); + + // Perform secondary scan to update line numbers for ignored entries if required + updateIgnoredFileDataOnLatestResult(tempFilePath.get(), proj, filePath); + + return scanResultAdaptor; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Secrets scanner error for " + filePath + ": " + e.getMessage(), e); + } finally { + CxLogger.warning(LOG_TAG + " Cleaning up temp folder: " + tempSubFolder); + deleteTempFolder(tempSubFolder); + } + } + return null; + } + + /** + * Compatibility method matching ScannerService interface. + */ + @Override + public ScanResult scan(String filePath) { + if (!shouldScanFile(filePath)) { + return null; + } + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument != null ? liveDocument : new Document(), project); + } + + /** + * Performs a full scan without passing the ignore file to update line numbers of ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +//// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.warning(LOG_TAG + " Secrets: Performing full scan without ignore file to update line numbers"); +// +// SecretsRealtimeResults fullScanResults = null; +// try { +// fullScanResults = CxWrapperFactory.build() +// .secretsRealtimeScan(tempFilePath, ""); +// } catch (Exception e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// +// if (fullScanResults != null) { +// SecretsScanResultAdaptor fullScanResultAdaptor = new SecretsScanResultAdaptor(fullScanResults, filePath); +// // Hook for updating ignored line markers if IgnoreManager is active +// } +// } + } + + /** + * Resolves a unique subfolder path for storing the temporary file. + */ + private Path getTempSubFolderPathAsPath(String originalFilePath) { + Path baseTempPath = getSecureTempDirectory(); + String safeFileName = toSafeTempFileName(originalFilePath); + return baseTempPath.resolve(safeFileName); + } + + /** + * Creates a deterministic, filesystem-safe file name containing base name and a hash suffix. + */ + private String toSafeTempFileName(String filePath) { + String baseName = Paths.get(filePath).getFileName().toString(); + String hash = generateFileHash(filePath); + return baseName + "-" + hash; + } + + /** + * Generates a 16-character SHA-256 hash derived from the relative file path, timestamp, and UUID. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + /** + * Saves the content into a temporary file inside the target subfolder. + */ + private Optional saveFileForScanning(Path tempSubFolder, String originalFilePath, String fileContent) throws IOException { + String fileName = Paths.get(originalFilePath).getFileName().toString(); + Path tempFilePath = tempSubFolder.resolve(fileName); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + return Optional.of(tempFilePath.toString()); + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + return Paths.get(tempOSPath, SECRETS_DIR).toAbsolutePath().normalize(); + } + + protected void createTempFolder(Path tempDir) { + if (!Files.exists(tempDir)) { + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } + } + } + + protected void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java new file mode 100644 index 00000000..185351f7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -0,0 +1,1589 @@ +package com.checkmarx.eclipse.devassist.ui.findings; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.part.ViewPart; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.PreferencesUtil; +import org.eclipse.ui.ide.IDE; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; + +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsContentProvider; +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsLabelProvider; +import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.utils.PluginUtils; +import com.checkmarx.eclipse.views.CheckmarxView; +import com.checkmarx.eclipse.views.actions.ActionOpenPreferencesPage; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; +import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.ui.findings.actions.VulnerabilityFilterAction; +import com.checkmarx.eclipse.devassist.ui.findings.actions.VulnerabilityFilterState; +import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore; +import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore.IgnoredProblemsListener; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; + + +/** + * Custom Findings View for displaying Checkmarx scan results. + * Extends {@link ViewPart} to provide a custom view in Eclipse. + * Manages a tree view of vulnerabilities with filtering and navigation capabilities. + * Uses {@link TreeViewer} for flexible tree rendering with custom providers. + */ +public class CxFindingsView extends ViewPart implements IgnoredProblemsListener { + + public static final String ID = "com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView"; + private org.osgi.service.event.EventHandler eventHandler; + private org.osgi.service.event.EventHandler settingsEventHandler; + + private TreeViewer treeViewer; + private Map> currentIssues = new HashMap<>(); + private IgnoredProblemsStore ignoredStore; + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + public static final Image FINDINGS_PROMOTIONAL_CUBE = createScaledImage("/icons/cx-one-assist-cube.png", 240); + + public CxFindingsView() { + super(); + } + + + @Override + public void createPartControl(Composite parent) { + this.parentComposite = parent; + + // Set a clean 1-column layout on parent + GridLayout parentLayout = new GridLayout(1, true); + parentLayout.marginWidth = 0; + parentLayout.marginHeight = 0; + parentLayout.horizontalSpacing = 0; + parentLayout.verticalSpacing = 0; + parent.setLayout(parentLayout); + + // Always subscribe to events first + subscribeToEventBroker(); + + // Register ignored problems listener + ignoredStore = IgnoredProblemsStore.getInstance(); + ignoredStore.addListener(this); + + // Initial render check + refreshViewMode(); + } + + + /** + * Loads an image and scales it down to the given max width (maintaining aspect ratio) + * if it is larger than that width. + */ + private static Image createScaledImage(String path, int maxWidth) { + Image original = Activator.getImageDescriptor(path).createImage(); + if (original.getBounds().width <= maxWidth) { + return original; + } + + double scale = (double) maxWidth / original.getBounds().width; + int scaledWidth = maxWidth; + int scaledHeight = (int) Math.round(original.getBounds().height * scale); + + ImageData scaledData = original.getImageData().scaledTo(scaledWidth, scaledHeight); + Image scaledImage = new Image(original.getDevice(), scaledData); + original.dispose(); + return scaledImage; + } + + /** + * Determines which panel to draw based on current credentials status. + */ + private void refreshViewMode() { + if (parentComposite == null || parentComposite.isDisposed()) { + return; + } + + if (!PluginUtils.areCredentialsDefined()) { + drawMissingCredentialsPanel(parentComposite); + } else { + loadCachedIssues(); + drawFindingsPanel(parentComposite); + } + } + + + /** + * Loads initial cached scan issues from workspace session properties. + */ + private void loadCachedIssues() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + Map> existingIssues = problemHolder.getAllScanIssues(); + if (existingIssues != null && !existingIssues.isEmpty()) { + this.currentIssues = existingIssues; + } + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error reading cached issues: " + e.getMessage()); + } + } + + + private Composite openSettingsComposite; + + /** + * Renders the missing credentials panel centered inside the view parent. + */ + private void drawMissingCredentialsPanel(Composite parent) { + // Dispose all existing UI components in the view container + for (Control child : parent.getChildren()) { + child.dispose(); + } + + clearToolbar(); + + openSettingsComposite = new Composite(parent, SWT.NONE); + openSettingsComposite.setLayout(new GridLayout(1, true)); + openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + + // Logo + final Label cxLogo = new Label(openSettingsComposite, SWT.NONE); + cxLogo.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + cxLogo.setImage(CheckmarxView.CHECKMARX_OPEN_SETTINGS_LOGO); + + // Open Settings Button + Button btn = new Button(openSettingsComposite, SWT.NONE); + btn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + btn.setText(PluginConstants.BTN_OPEN_SETTINGS); + + btn.addListener(SWT.Selection, event -> { + PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn( + shell, "com.checkmarx.eclipse.properties.preferencespage", null, null); + if (pref != null) { + pref.open(); + } + }); + + parent.layout(true, true); + } + + private void drawFindingsPanel(Composite parent) { + // Clear out missing credentials panel if it exists + for (Control child : parent.getChildren()) { + child.dispose(); + } + + SashForm sashForm = new SashForm(parent, SWT.HORIZONTAL); + sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + sashForm.setLayout(new FillLayout()); + + Composite treeComposite = new Composite(sashForm, SWT.NONE); + treeComposite.setLayout(new FillLayout()); + + treeViewer = new TreeViewer(treeComposite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + treeViewer.setContentProvider(new FindingsContentProvider()); + treeViewer.setLabelProvider(new FindingsLabelProvider()); + + Composite promotionalComposite = new Composite(sashForm, SWT.NONE); + drawPromotionalPanel(promotionalComposite); + + sashForm.setWeights(new int[] { 70, 30 }); + + setupToolbar(); + setupTreeListeners(); + + if (!currentIssues.isEmpty()) { + refreshTreeWithFilter(); + } + + parent.layout(true, true); + } + + /** + * Renders the promotional cube image and description text in the right-hand pane + * of the findings split view. + */ + private void drawPromotionalPanel(Composite promotionalComposite) { + GridLayout layout = new GridLayout(1, false); + layout.marginLeft = 0; + layout.marginRight = 40; + layout.marginHeight = 10; + promotionalComposite.setLayout(layout); + + Label cubeLabel = new Label(promotionalComposite, SWT.NONE); + cubeLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + cubeLabel.setImage(FINDINGS_PROMOTIONAL_CUBE); + Label descriptionLabel = new Label(promotionalComposite, SWT.WRAP); + GridData descriptionData = new GridData(SWT.LEFT, SWT.CENTER, true, false); + descriptionLabel.setLayoutData(descriptionData); + descriptionLabel.setText(PluginConstants.FINDINGS_PROMO_DESCRIPTION); + + // SWT.WRAP labels need an explicit widthHint to wrap and left-align under the image + // instead of growing to one unbroken line. The pane has no real bounds yet at this + // point (the parent hasn't laid out), so compute it once asynchronously after the + // initial layout, and again whenever the pane is resized (e.g. by dragging the sash). + Runnable applyWrapWidth = () -> { + if (promotionalComposite.isDisposed()) { + return; + } + int availableWidth = promotionalComposite.getClientArea().width + - (layout.marginLeft + layout.marginRight); + if (availableWidth > 0 && descriptionData.widthHint != availableWidth) { + descriptionData.widthHint = availableWidth; + promotionalComposite.layout(true); + } + }; + + promotionalComposite.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + applyWrapWidth.run(); + } + }); + Display.getDefault().asyncExec(applyWrapWidth); + } + + /** + * Subscribes to IEventBroker for issue updates & settings changes. + */ + private void subscribeToEventBroker() { + try { + org.eclipse.e4.core.services.events.IEventBroker eventBroker = + getSite().getService(org.eclipse.e4.core.services.events.IEventBroker.class); + + if (eventBroker == null) { + eventBroker = PlatformUI.getWorkbench().getService( + org.eclipse.e4.core.services.events.IEventBroker.class); + } + + if (eventBroker != null) { + // Topic 1: Scan issues updated + eventHandler = event -> { + Object data = event.getProperty(org.eclipse.e4.core.services.events.IEventBroker.DATA); + if (data instanceof Map) { + @SuppressWarnings("unchecked") + Map> newIssues = (Map>) data; + + Display.getDefault().asyncExec(() -> { + this.currentIssues = newIssues; + if (treeViewer != null && !treeViewer.getControl().isDisposed()) { + refreshTreeWithFilter(); + } + }); + } + }; + eventBroker.subscribe(ProblemHolderService.ISSUES_UPDATED_TOPIC, eventHandler); + + // Topic 2: Settings/Credentials applied or changed + settingsEventHandler = event -> { + Display.getDefault().asyncExec(() -> { + refreshViewMode(); + }); + }; + eventBroker.subscribe(PluginConstants.TOPIC_APPLY_SETTINGS, settingsEventHandler); + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error subscribing to IEventBroker: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Override + public void dispose() { + + + // 1. Unsubscribe from IEventBroker to prevent memory leaks + if (eventHandler != null) { + try { + org.eclipse.e4.core.services.events.IEventBroker eventBroker = + org.eclipse.ui.PlatformUI.getWorkbench().getService( + org.eclipse.e4.core.services.events.IEventBroker.class); + + if (eventBroker != null) { + eventBroker.unsubscribe(eventHandler); + + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error unsubscribing from IEventBroker: " + e.getMessage()); + } + } + + // 2. Unsubscribe from IgnoredProblemsStore + if (ignoredStore != null) { + // If your IgnoredProblemsStore supports removing listeners, call it here: + // ignoredStore.removeListener(this); + } + + super.dispose(); + } + + private Composite parentComposite; + + + private void initFindingsViewUI() { + try { + + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + Map> existingIssues = problemHolder.getAllScanIssues(); + if (existingIssues != null && !existingIssues.isEmpty()) { + this.currentIssues = existingIssues; + } + } + } + + subscribeToEventBroker(); + + ignoredStore = IgnoredProblemsStore.getInstance(); + ignoredStore.addListener(this); + + drawFindingsPanel(parentComposite); + + } catch (Exception e) { + System.err.println("[FINDINGS] Error during view creation: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Removes every contribution from the view toolbar. + */ + private void clearToolbar() { + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + toolbar.removeAll(); + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + } + + private void setupToolbar() { + + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + + // The same IToolBarManager instance survives every re-render of the view, + // so previous contributions must be dropped before re-adding them. + toolbar.removeAll(); + + // Add filter actions + VulnerabilityFilterAction.IFilterChangeListener filterListener = () -> { + + refreshTreeWithFilter(); + }; + + toolbar.add(new VulnerabilityFilterAction.MaliciousFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.CriticalFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.HighFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.MediumFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.LowFilter(filterListener)); + + toolbar.add(new org.eclipse.jface.action.Separator("\t")); + + // Shared Eclipse images (replace with your own icons later) + ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); + + // Toggle Expand/Collapse action + Action toggleExpandCollapseAction = new Action("Expand All", Action.AS_PUSH_BUTTON) { + + private boolean expanded = false; + + { + setToolTipText("Collapse All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL)); + } + + @Override + public void run() { + if (expanded) { + treeViewer.collapseAll(); + setText("Expand All"); + setToolTipText("Expand All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED)); + } else { + treeViewer.expandAll(); + setText("Collapse All"); + setToolTipText("Collapse All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL)); + } + + expanded = !expanded; + } + }; + + toolbar.add(toggleExpandCollapseAction); + + // Add spacing before preferences button + toolbar.add(new org.eclipse.jface.action.Separator("\t")); + + // Preferences action (same implementation as CheckmarxView) + Action openPreferencesPageAction = + new ActionOpenPreferencesPage( + null, + treeViewer, + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell()) + .createAction(); + + // Toolbar preferences button + Action toolbarPreferencesAction = + new Action("\u2000?", Action.AS_PUSH_BUTTON) { + @Override + public void run() { + openPreferencesPageAction.run(); + } + }; + + toolbarPreferencesAction.setToolTipText("Checkmarx Preferences"); + toolbar.add(toolbarPreferencesAction); + + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + + } + private void setupTreeListeners() { + Tree tree = treeViewer.getTree(); + + + //Listner for redirection + tree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + + navigateToSelectedIssue(treeViewer.getSelection()); + } + }); + + // Right-click context menu + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(MouseEvent e) { + if (e.button == 3) { + + showContextMenu(e); + } + } + }); + + + } + + private void navigateToSelectedIssue(ISelection selection) { + + if (selection instanceof IStructuredSelection) { + IStructuredSelection ssel = (IStructuredSelection) selection; + Object element = ssel.getFirstElement(); + + if (element instanceof ScanDetailWithPath) { + ScanDetailWithPath detailWithPath = (ScanDetailWithPath) element; + navigateToIssue(detailWithPath); + } + } + } + + private void navigateToIssue(ScanDetailWithPath detailWithPath) { + ScanIssue detail = detailWithPath.getDetail(); + + // Use resolved file path from ScanIssue (if available) or fallback to detailWithPath + String filePath = detail.getFilePath(); + if (filePath == null) { + filePath = detailWithPath.getFilePath(); + } + + if (detail.getLocations() != null && !detail.getLocations().isEmpty()) { + Location location = detail.getLocations().get(0); + + openFileInEditor(filePath, location.getLine(), detail); + } else { + + } + } + + /** + * Show detailed information about an issue. + */ + private void showIssueDetails(ScanIssue issue) { + StringBuilder details = new StringBuilder(); + details.append("\n========== ISSUE DETAILS ==========\n"); + details.append("Title: ").append(issue.getTitle()).append("\n"); + details.append("Severity: ").append(issue.getSeverity()).append("\n"); + details.append("Scan Engine: ").append(issue.getScanEngine()).append("\n"); + details.append("Description: ").append(issue.getDescription()).append("\n"); + details.append("Issue ID: ").append(issue.getScanIssueId()).append("\n"); + + if (issue.getPackageVersion() != null) { + details.append("Package Version: ").append(issue.getPackageVersion()).append("\n"); + } + if (issue.getCve() != null) { + details.append("CVE: ").append(issue.getCve()).append("\n"); + } + if (issue.getRemediationAdvise() != null) { + details.append("Remediation: ").append(issue.getRemediationAdvise()).append("\n"); + } + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location loc = issue.getLocations().get(0); + details.append("Location: Line ").append(loc.getLine()).append(", Col ").append(loc.getStartIndex()).append("\n"); + } + details.append("====================================\n"); + + + } + + /** + * Fix issue with AI Assist. + */ + private void fixWithAIAssist(ScanIssue issue) { + + try { + // Build remediation prompt based on engine type + String prompt = com.checkmarx.eclipse.devassist.ui.findings.integration.RemediationPromptBuilder + .buildRemediationPrompt(issue); + + if (prompt == null || prompt.isEmpty()) { + + showErrorNotification("Failed to build prompt for this issue type"); + return; + } + + // Send to Copilot via integration + + boolean success = com.checkmarx.eclipse.devassist.ui.findings.integration.CopilotIntegration + .sendPromptToCopilot(prompt); + + if (success) { + + } else { + + } + + } catch (Exception e) { + + e.printStackTrace(); + showErrorNotification("Error: " + e.getMessage()); + } + } + + /** + * Show error notification to user + */ + private void showErrorNotification(String message) { + org.eclipse.swt.widgets.MessageBox msgBox = new org.eclipse.swt.widgets.MessageBox( + treeViewer.getTree().getShell(), + org.eclipse.swt.SWT.ERROR); + msgBox.setMessage(message); + msgBox.setText("Checkmarx AI Assist"); + msgBox.open(); + } + + /** + * Ignore this specific finding and remove from the Findings View. + * The finding is added to the IgnoredProblemsStore and appears in the Ignored Problems Window. + */ + private void ignoreThisFinding(ScanIssue issue) { + + try { + // Verify store is initialized + if (ignoredStore == null) { + System.err.println("[FINDINGS] ERROR: IgnoredProblemsStore is NULL!"); + showErrorNotification("Error: IgnoredProblemsStore not initialized"); + return; + } + + + + // Add to ignored store with full finding details for display in Ignored Problems View + ignoredStore.ignoreProblem(issue); + + + // Check if it was actually added + boolean isIgnored = ignoredStore.isIgnored(issue.getScanIssueId()); + + // Refresh the tree to remove the ignored finding + + refreshTreeWithFilter(); + + + + } catch (Exception e) { + System.err.println("[FINDINGS] ✗ Error ignoring finding: " + e.getMessage()); + e.printStackTrace(); + showErrorNotification("Failed to ignore finding: " + e.getMessage()); + } + } + + /** + * Ignore all findings of the same type/package. + * For OSS: ignores all findings with the same package version + * For CONTAINERS: ignores all findings with the same image tag + */ + private void ignoreAllOfType(ScanIssue issue) { + + try { + int ignoredCount = 0; + String typeIdentifier = issue.getPackageVersion() != null ? issue.getPackageVersion() : issue.getImageTag(); + + // Iterate through all current issues and ignore matching ones + for (List issues : currentIssues.values()) { + for (ScanIssue currentIssue : issues) { + // Match by same type/package/image + if (currentIssue.getScanEngine() == issue.getScanEngine()) { + String currentTypeIdentifier = currentIssue.getPackageVersion() != null ? + currentIssue.getPackageVersion() : currentIssue.getImageTag(); + + if (typeIdentifier != null && typeIdentifier.equals(currentTypeIdentifier)) { + ignoredStore.ignoreProblem(currentIssue); + ignoredCount++; + } + } + } + } + + + refreshTreeWithFilter(); + + + } catch (Exception e) { + System.err.println("[FINDINGS] ✗ Error ignoring findings of type: " + e.getMessage()); + e.printStackTrace(); + showErrorNotification("Failed to ignore findings of this type: " + e.getMessage()); + } + } + + /** + * Copy issue details to clipboard as JSON. + */ + private void copyIssueDetails(ScanIssue issue) { + StringBuilder json = new StringBuilder(); + json.append("{\n"); + json.append(" \"title\": \"").append(escapeJson(issue.getTitle())).append("\",\n"); + json.append(" \"severity\": \"").append(issue.getSeverity()).append("\",\n"); + json.append(" \"scanEngine\": \"").append(issue.getScanEngine()).append("\",\n"); + json.append(" \"description\": \"").append(escapeJson(issue.getDescription())).append("\",\n"); + json.append(" \"issueId\": \"").append(issue.getScanIssueId()).append("\"\n"); + json.append("}\n"); + + try { + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new java.awt.datatransfer.StringSelection(json.toString()), null); + + + } catch (Exception e) { + + } + } + + private String escapeJson(String text) { + if (text == null) return ""; + return text.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); + } + + private void openFileInEditor(String filePath, int lineNumber, ScanIssue issue) { + try { + + + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( + new org.eclipse.core.runtime.Path(filePath)); + + if (file == null || !file.exists()) { + + return; + } + + // 1. Open file in active workbench page + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IEditorPart editor = IDE.openEditor(page, file); + + + // **CRITICAL FIX: Navigation-based opens don't trigger IPartListener2 events** + // Directly set up real-time scanning and apply cached decorations + // Pass the editor to avoid re-searching for it (which fails on MavenPomEditor) + setupRealtimeScanningForFile(file, editor); + + // 2. Ensure marker exists and explicitly set LINE_NUMBER + createMarkerForIssue(file, issue); + + // 3. Navigate using standard ITextEditor adapter (or fall back to marker navigation) + boolean scrolledSuccessfully = scrollToLine(editor, lineNumber); + if (!scrolledSuccessfully) { + highlightViaMarker(editor, file, issue); + } + + } catch (Exception e) { + + e.printStackTrace(); + } + } + + /** + * Set up real-time scanning and apply cached decorations for a file. + * Called when file is opened via navigation to ensure we don't miss IPartListener2 events. + */ + private void setupRealtimeScanningForFile(org.eclipse.core.resources.IFile file, IEditorPart editor) { + if (file == null || editor == null) { + + return; + } + + + + + try { + // Extract document for real-time scanning + org.eclipse.jface.text.IDocument document = null; + String filePath = file.getLocation().toOSString(); + String fileName = file.getName(); + + + + // Try method 1: Direct ITextEditor instance check + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + + // Try method 2: ITextEditor Adapter pattern (for MavenPomEditor, etc.) + if (document == null) { + + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor != null) { + + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + } + + // Try method 3: Direct IDocument adapter (some editors provide this directly) + if (document == null) { + + document = editor.getAdapter(org.eclipse.jface.text.IDocument.class); + if (document != null) { + + } + } + + if (document == null) { + + return; + } + + + + + + // Create a scan job for this file + com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob scanJob = + new com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob(file, fileName); + + + + + + // Create a document listener that reschedules the job on every keystroke + 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) { + // Ignore if scheduler not available + } + } + com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxDocumentListener docListener = + new com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxDocumentListener(fileName, scanJob, file, scheduler); + + // Register the document listener + document.addDocumentListener(docListener); + + + + + // Apply cached decorations if findings exist for this file + // Pass the editor directly to avoid search issues with MavenPomEditor + applyCachedDecorationsForFile(file, document, editor); + + + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] ✗ EXCEPTION during setup: " + e.getMessage()); + System.err.println("[REALTIME-SETUP] Exception type: " + e.getClass().getName()); + System.err.println("[REALTIME-SETUP] Stack trace:"); + e.printStackTrace(); + } + } + + /** + * Apply cached decorations (gutter icons, underlines) when editor is opened via navigation. + * Uses the provided editor directly instead of searching for it. + */ + private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, + org.eclipse.jface.text.IDocument document, + org.eclipse.ui.IEditorPart editor) { + if (file == null || document == null || editor == 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 directly using the provided editor + + applyDecorationsDirectly(editor, file, cachedIssues); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] Error applying cached decorations: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Apply decorations directly to the provided editor without searching for it. + * This avoids issues with MavenPomEditor not being found by IFile comparison. + */ + private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, + org.eclipse.core.resources.IFile file, + java.util.List scanIssues) { + if (editor == null || file == null || scanIssues == null || scanIssues.isEmpty()) { + return; + } + + try { + // Get the text editor + org.eclipse.ui.texteditor.ITextEditor textEditor = + editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + + if (textEditor == null) { + + return; + } + + // Get document provider and input + org.eclipse.ui.texteditor.IDocumentProvider docProvider = textEditor.getDocumentProvider(); + if (docProvider == null) { + + return; + } + + // Get annotation model from the document provider (proper way for all editor types) + org.eclipse.jface.text.source.IAnnotationModel annotationModel = + docProvider.getAnnotationModel(textEditor.getEditorInput()); + + if (annotationModel == null) { + + return; + } + + + + // Get document from provider + org.eclipse.jface.text.IDocument document = docProvider.getDocument(textEditor.getEditorInput()); + + if (document == null) { + + return; + } + + // Apply each issue's decoration using OSS-specific logic + java.util.List annotations = + new java.util.ArrayList<>(); + + for (ScanIssue issue : scanIssues) { + try { + // Create annotation + com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation annotation = + createAnnotationForIssue(issue); + + if (annotation == null) { + continue; + } + + // Calculate position (OSS = first line only) + org.eclipse.jface.text.Position pos = calculatePositionForIssue(document, issue); + + if (pos != null && pos.getLength() > 0) { + annotationModel.addAnnotation(annotation, pos); + annotations.add(annotation); + + } + } catch (Exception e) { + System.err.println("[REALTIME-SETUP-DIRECT] Error decorating issue: " + e.getMessage()); + } + } + + + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP-DIRECT] ✗ Error applying decorations directly: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Create annotation for an issue. + */ + private com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation createAnnotationForIssue(ScanIssue issue) { + try { + String annotationType = mapSeverityToAnnotationType(issue.getSeverity()); + return new com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation( + annotationType, + issue.getTitle(), + issue.getDescription() + ); + } catch (Exception e) { + return null; + } + } + + /** + * Map severity to annotation type. + * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. + */ + private String mapSeverityToAnnotationType(String severity) { + if (severity == null) { + return "com.checkmarx.eclipse.findings.unknown"; + } + String upper = severity.toUpperCase(); + if (upper.contains("MALICIOUS")) { + return "com.checkmarx.eclipse.findings.malicious"; + } + if (upper.contains("CRITICAL") || upper.contains("ERROR")) { + return "com.checkmarx.eclipse.findings.critical"; + } + if (upper.contains("HIGH")) { + return "com.checkmarx.eclipse.findings.high"; + } + if (upper.contains("MEDIUM")) { + return "com.checkmarx.eclipse.findings.medium"; + } + if (upper.contains("LOW") || upper.contains("INFO")) { + return "com.checkmarx.eclipse.findings.low"; + } + if (upper.contains("UNKNOWN")) { + return "com.checkmarx.eclipse.findings.unknown"; + } + if (upper.contains("OK")) { + return "com.checkmarx.eclipse.findings.ok"; + } + if (upper.contains("IGNORED")) { + return "com.checkmarx.eclipse.findings.ignored"; + } + return "com.checkmarx.eclipse.findings.unknown"; + } + + /** + * Calculate position for an issue (OSS = first line only). + */ + private org.eclipse.jface.text.Position calculatePositionForIssue(org.eclipse.jface.text.IDocument document, ScanIssue issue) { + try { + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + return null; + } + + com.checkmarx.eclipse.devassist.model.Location location = issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // 0-based + + int lineCount = document.getNumberOfLines(); + if (lineNumber < 0 || lineNumber >= lineCount) { + return null; + } + + org.eclipse.jface.text.IRegion lineInfo = document.getLineInformation(lineNumber); + int offset = lineInfo.getOffset(); + int length = lineInfo.getLength(); + + // FIX: Skip leading whitespace to match ProblemDecorator.calculateRange() + int trimOffset = getLeadingWhitespaceOffset(document, offset, length); + int adjustedOffset = offset + trimOffset; + int adjustedLength = Math.max(1, length - trimOffset); + + return new org.eclipse.jface.text.Position(adjustedOffset, adjustedLength); + + } catch (Exception e) { + return null; + } + } + + /** + * Scroll editor to specific line number using native Eclipse ITextEditor adapter. + */ + private boolean scrollToLine(IEditorPart editor, int lineNumber) { + if (editor == null || lineNumber <= 0) return false; + + try { + // Use Eclipse's standard adapter pattern instead of reflection + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor == null && editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + } + + if (textEditor != null) { + org.eclipse.ui.texteditor.IDocumentProvider provider = textEditor.getDocumentProvider(); + if (provider != null) { + org.eclipse.jface.text.IDocument document = provider.getDocument(textEditor.getEditorInput()); + if (document != null && lineNumber <= document.getNumberOfLines()) { + // Line numbers in IDocument are 0-indexed + int lineOffset = document.getLineOffset(lineNumber - 1); + textEditor.selectAndReveal(lineOffset, 0); + + return true; + } + } + } + } catch (Exception e) { + + } + return false; + } + + /** + * Ensures IMarker.LINE_NUMBER is explicitly set as a 1-based Integer attribute. + */ + private void createMarkerForIssue(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + + try { + IMarker existingMarker = findMarkerForIssue(file, issue); + if (existingMarker != null && existingMarker.exists()) { + return; + } + + // 1. Create the marker using the declared ID + IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); + + // 2. Set Standard Core Eclipse Attributes (CRITICAL for Quick Fix matching) + int lineNumber = issue.getLocations().get(0).getLine(); + newMarker.setAttribute(IMarker.LINE_NUMBER, lineNumber > 0 ? lineNumber : 1); + newMarker.setAttribute(IMarker.MESSAGE, issue.getTitle() != null ? issue.getTitle() : "Checkmarx Finding"); + newMarker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING); + newMarker.setAttribute(IMarker.USER_EDITABLE, false); + + // FIX: Set character offsets with whitespace trimming (so underline doesn't include leading spaces) + setMarkerCharacterOffsets(newMarker, file, lineNumber); + + // 3. Populate custom attributes + com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); + + + + } catch (org.eclipse.core.runtime.CoreException e) { + System.err.println("[FINDINGS] Error creating marker: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Apply highlighting to the problematic line. + */ + /** + * Navigate to the marker that corresponds to this issue. + * JDT's editor will automatically underline the marker and respect + * the marker annotation infrastructure (no custom hover registration needed). + */ + private void highlightViaMarker(org.eclipse.ui.IEditorPart editor, IFile file, ScanIssue issue) { + try { + if (editor == null || file == null || issue == null) { + return; + } + + // Find the marker corresponding to this issue + IMarker marker = findMarkerForIssue(file, issue); + if (marker != null && marker.exists()) { + org.eclipse.ui.ide.IDE.gotoMarker(editor, marker); + + } else { + + } + } catch (Exception e) { + + } + } + + /** + * Find the IMarker that corresponds to a ScanIssue. + * Matches by file, line number, and optionally title. + */ + private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return null; + } + + int issueLine = issue.getLocations().get(0).getLine(); + String issueTitle = issue.getTitle(); + + try { + IMarker[] markers = file.findMarkers("com.checkmarx.eclipse.plugin.checkmarxProblemMarker", true, org.eclipse.core.resources.IResource.DEPTH_ZERO); + for (IMarker marker : markers) { + int markerLine = marker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); + if (markerLine == issueLine) { + // Optional: also match by message prefix for better accuracy + String markerMsg = marker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); + if (issueTitle == null || issueTitle.isEmpty() || markerMsg.contains(issueTitle)) { + return marker; + } + } + } + } catch (Exception e) { + + } + + return null; + } + + /** + * Create a marker for a ScanIssue. + * + * **CRITICAL FIX**: Markers were never being created, only searched for. + * This method creates markers on-demand when user navigates to an issue. + * + * **Works for ALL file types**: Java, Python, C++, JavaScript, YAML, XML, etc. + * Uses Eclipse's universal IMarker API (not language-specific). + * + * Marker attributes are populated using MarkerIssueMapper to store + * all ScanIssue data in marker attributes for later retrieval. + * + * @param file File to create marker in + * @param issue ScanIssue to create marker for + */ +// private void createMarkerForIssue(IFile file, ScanIssue issue) { +// if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { +// +// return; +// } +// +// try { +// +// +// +// +// +// +// +// +// // Step 1: Check if marker already exists for this issue +// IMarker existingMarker = findMarkerForIssue(file, issue); +// if (existingMarker != null && existingMarker.exists()) { +// +// return; +// } +// +// // Step 2: Create new marker using Eclipse's universal IMarker API +// // **KEY**: Uses IMarker.PROBLEM which works for ALL file types +// // - NOT language-specific (works for Java, Python, C++, JS, YAML, etc.) +// // - Marker appears in Eclipse's Problems View +// // - Can be navigated with IDE.gotoMarker() +// IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); +// +// +// // Step 3: Populate marker attributes using MarkerIssueMapper +// // This stores all ScanIssue data in marker for later retrieval +// com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); +// +// +// // Step 4: Verify marker creation +// if (newMarker.exists()) { +// String markerMsg = newMarker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); +// int markerLine = newMarker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); +// int markerSeverity = newMarker.getAttribute(org.eclipse.core.resources.IMarker.SEVERITY, -1); +// +// +// +// +// +// +// +// } else { +// +// } +// +// } catch (org.eclipse.core.runtime.CoreException e) { +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ CoreException creating marker: " + e.getMessage()); +// e.printStackTrace(); +// } catch (Exception e) { +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ Error creating marker: " + e.getMessage()); +// e.printStackTrace(); +// } +// } + + private void showContextMenu(MouseEvent e) { + ISelection selection = treeViewer.getSelection(); + if (!(selection instanceof IStructuredSelection)) { + + return; + } + + IStructuredSelection ssel = (IStructuredSelection) selection; + Object element = ssel.getFirstElement(); + + if (!(element instanceof ScanDetailWithPath)) { + + return; + } + + ScanDetailWithPath detailWithPath = (ScanDetailWithPath) element; + ScanIssue issue = detailWithPath.getDetail(); + + + + org.eclipse.swt.widgets.Menu menu = new org.eclipse.swt.widgets.Menu(treeViewer.getTree()); + + // Menu Item 1: View Details + org.eclipse.swt.widgets.MenuItem viewDetailsItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + viewDetailsItem.setText("View Details"); + viewDetailsItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + showIssueDetails(issue); + } + }); + + // Menu Item 2: Fix with AI Assist + org.eclipse.swt.widgets.MenuItem fixWithAIItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + fixWithAIItem.setText("Fix with AI Assist"); + fixWithAIItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + fixWithAIAssist(issue); + } + }); + + // Menu Item 3: Ignore This Finding + org.eclipse.swt.widgets.MenuItem ignoreItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + ignoreItem.setText("Ignore This Finding"); + ignoreItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + ignoreThisFinding(issue); + } + }); + + // Menu Item 4: Ignore All of This Type (for OSS and CONTAINERS) + if (issue.getScanEngine() == ScanEngine.OSS || issue.getScanEngine() == ScanEngine.CONTAINERS) { + org.eclipse.swt.widgets.MenuItem ignoreAllItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + ignoreAllItem.setText("Ignore All of This Type"); + ignoreAllItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + ignoreAllOfType(issue); + } + }); + } + + // Separator + new org.eclipse.swt.widgets.MenuItem(menu, SWT.SEPARATOR); + + // Menu Item 5: Copy Issue Details + org.eclipse.swt.widgets.MenuItem copyItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + copyItem.setText("Copy Issue Details (JSON)"); + copyItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + copyIssueDetails(issue); + } + }); + + // Menu Item 6: Open in Terminal + org.eclipse.swt.widgets.MenuItem terminalItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + terminalItem.setText("Navigate to Line"); + terminalItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + + navigateToIssue(detailWithPath); + } + }); + + menu.setLocation(treeViewer.getTree().toDisplay(e.x, e.y)); + menu.setVisible(true); + } + + private void refreshTreeWithFilter() { + + + + + // Apply active filters and refresh + VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); + + + Map> filteredIssues = new HashMap<>(); + int totalBefore = 0; + int totalAfter = 0; + + for (String filePath : currentIssues.keySet()) { + List issues = currentIssues.get(filePath); + if (issues == null) continue; + + totalBefore += issues.size(); + List filtered = new java.util.ArrayList<>(); + + for (ScanIssue issue : issues) { + // ✅ Safe null guard FIRST before calling any methods on issue + if (issue == null || issue.getSeverity() == null) { + + continue; + } + + String issueId = issue.getScanIssueId(); + boolean isIgnored = ignoredStore != null && ignoredStore.isIgnored(issueId); + boolean hasFilter = filterState.hasFilter(issue.getSeverity()); + boolean isProblem = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.isProblem(issue.getSeverity()); + + + + // Filter by OK/UNKNOWN/IGNORED severity (Phase 3) + if (!isProblem) { + + continue; + } + // Filter by severity preference + if (!hasFilter) { + + continue; + } + // Filter out ignored problems + if (isIgnored) { + + continue; + } + + + filtered.add(issue); + } + + if (!filtered.isEmpty()) { + filteredIssues.put(filePath, filtered); + totalAfter += filtered.size(); + } + } + + + + + + // ✅ Verify treeViewer control before manipulating UI + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + + // Save current expansion state to avoid full tree rebuild + Object[] expandedElements = treeViewer.getExpandedElements(); + + // Use setInput() for initial population, refresh() for subsequent updates + Object currentInput = treeViewer.getInput(); + if (currentInput == null) { + // First time: full tree setup with initial data + treeViewer.setInput(filteredIssues); + treeViewer.expandAll(); + } else { + // Subsequent updates: use targeted refresh instead of full rebuild + // This avoids rebuilding the entire tree on every single-file scan + treeViewer.setInput(filteredIssues); + + // Restore expansion state for files that still exist in filtered results + java.util.List validExpanded = new java.util.ArrayList<>(); + for (Object element : expandedElements) { + if (element instanceof com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel) { + com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel fileNode = + (com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel) element; + if (filteredIssues.containsKey(fileNode.getFilePath())) { + validExpanded.add(element); + } + } + } + + if (!validExpanded.isEmpty()) { + treeViewer.setExpandedElements(validExpanded.toArray()); + } else { + // If no previous expansion state, expand all + treeViewer.expandAll(); + } + } + } + + // Update view title with problem count + setPartName("Checkmarx One Assist Findings " + totalAfter); + } + + /** + * Refresh the tree with new issues. Safely dispatches to the SWT UI Thread. + * + * @param issues Map of file paths to list of scan issues + */ + public void refreshTree(Map> issues) { + if (issues == null) return; + + + + + + int totalIssues = issues.values().stream().filter(java.util.Objects::nonNull).mapToInt(List::size).sum(); + + + // Log issues by severity + Map severityCounts = new HashMap<>(); + issues.values().forEach(issueList -> { + if (issueList != null) { + issueList.forEach(issue -> { + if (issue != null && issue.getSeverity() != null) { + String severity = issue.getSeverity().toLowerCase(); + severityCounts.put(severity, severityCounts.getOrDefault(severity, 0L) + 1); + } + }); + } + }); + + + + for (String filePath : issues.keySet()) { + List fileIssues = issues.get(filePath); + } + + + this.currentIssues = issues; + + // ✅ Thread-safe dispatching for background updates + org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + refreshTreeWithFilter(); + } + }); + + + } + + @Override + public void setFocus() { + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + treeViewer.getControl().setFocus(); + } + } + + public TreeViewer getTreeViewer() { + return treeViewer; + } + + /** + * Listener implementation: called when ignored problems are restored or cleared. + */ + @Override + public void onIgnoredProblemsChanged() { + + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + treeViewer.getControl().getDisplay().asyncExec(this::refreshTreeWithFilter); + } + } + + + private void setMarkerCharacterOffsets(IMarker marker, IFile file, int lineNumber) { + try { + org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window == null) return; + org.eclipse.ui.IWorkbenchPage page = window.getActivePage(); + if (page == null) return; + org.eclipse.ui.IEditorPart editor = page.getActiveEditor(); + if (editor == null) return; + + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor == null) return; + + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + if (doc == null || lineNumber <= 0 || lineNumber > doc.getNumberOfLines()) return; + + int lineIdx = lineNumber - 1; + int lineOffset = doc.getLineOffset(lineIdx); + int lineLen = doc.getLineLength(lineIdx); + + int trimOffset = getLeadingWhitespaceOffset(doc, lineOffset, lineLen); + marker.setAttribute(IMarker.CHAR_START, lineOffset + trimOffset); + marker.setAttribute(IMarker.CHAR_END, lineOffset + lineLen); + + } catch (Exception e) { + // If we can't set char offsets, marker will still work with line-based positioning + } + } + + private int getLeadingWhitespaceOffset(org.eclipse.jface.text.IDocument document, int lineOffset, int lineLength) { + try { + String lineText = document.get(lineOffset, lineLength); + int count = 0; + for (int i = 0; i < lineText.length(); i++) { + if (!Character.isWhitespace(lineText.charAt(i))) break; + count++; + } + return count; + } catch (Exception e) { + return 0; + } + } + +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java new file mode 100644 index 00000000..028e9561 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java @@ -0,0 +1,80 @@ +package com.checkmarx.eclipse.devassist.ui.findings.actions; + +import org.eclipse.jface.action.Action; + +/** + * Base toggle action for severity filters in the Findings view. + */ +public abstract class VulnerabilityFilterAction extends Action { + + private final String severity; + private final IFilterChangeListener filterChangeListener; + + public interface IFilterChangeListener { + void onFilterChanged(); + } + + public VulnerabilityFilterAction(String severity, IFilterChangeListener listener) { + super(severity, Action.AS_CHECK_BOX); + this.severity = severity; + this.filterChangeListener = listener; + + setText(severity.substring(0, 1).toUpperCase() + severity.substring(1)); + setImageDescriptor(org.eclipse.ui.plugin.AbstractUIPlugin + .imageDescriptorFromPlugin("com.checkmarx.eclipse.plugin", + "icons/severity/" + severity + "_20.svg")); + setToolTipText("Filter " + severity + " severity findings"); + + // Set initial state + setChecked(VulnerabilityFilterState.getInstance().hasFilter(severity)); + } + + @Override + public void run() { + VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); + if (isChecked()) { + filterState.addFilter(severity); + } else { + filterState.removeFilter(severity); + } + + if (filterChangeListener != null) { + filterChangeListener.onFilterChanged(); + } + } + + public String getSeverity() { + return severity; + } + + // Concrete implementations for each severity level + public static class MaliciousFilter extends VulnerabilityFilterAction { + public MaliciousFilter(IFilterChangeListener listener) { + super("malicious", listener); + } + } + + public static class CriticalFilter extends VulnerabilityFilterAction { + public CriticalFilter(IFilterChangeListener listener) { + super("critical", listener); + } + } + + public static class HighFilter extends VulnerabilityFilterAction { + public HighFilter(IFilterChangeListener listener) { + super("high", listener); + } + } + + public static class MediumFilter extends VulnerabilityFilterAction { + public MediumFilter(IFilterChangeListener listener) { + super("medium", listener); + } + } + + public static class LowFilter extends VulnerabilityFilterAction { + public LowFilter(IFilterChangeListener listener) { + super("low", listener); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java new file mode 100644 index 00000000..9e2b7d21 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java @@ -0,0 +1,67 @@ +package com.checkmarx.eclipse.devassist.ui.findings.actions; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Holds filter state (set of severity levels) for the Findings view. + * Singleton pattern for managing active filters across the application. + */ +public class VulnerabilityFilterState { + + private static final VulnerabilityFilterState INSTANCE = new VulnerabilityFilterState(); + + private final Set selectedFilters = Collections.synchronizedSet(new HashSet<>()); + + private VulnerabilityFilterState() { + // Initialize with default filters (all severities) + selectedFilters.add("malicious"); + selectedFilters.add("critical"); + selectedFilters.add("high"); + selectedFilters.add("medium"); + selectedFilters.add("low"); + } + + public static VulnerabilityFilterState getInstance() { + return INSTANCE; + } + + public Set getFilters() { + return selectedFilters; + } + + public void addFilter(String severity) { + if (severity != null) { + selectedFilters.add(severity.toLowerCase()); + } + } + + public void removeFilter(String severity) { + if (severity != null) { + selectedFilters.remove(severity.toLowerCase()); + } + } + + public boolean hasFilter(String severity) { + if (severity == null) { + + return false; + } + boolean result = selectedFilters.contains(severity.toLowerCase()); + return result; + } + + public void clearFilters() { + selectedFilters.clear(); + } + + public void resetToDefaults() { + selectedFilters.clear(); + selectedFilters.add("malicious"); + selectedFilters.add("critical"); + selectedFilters.add("high"); + selectedFilters.add("medium"); + selectedFilters.add("low"); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java new file mode 100644 index 00000000..5dc0a5f1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java @@ -0,0 +1,181 @@ +package com.checkmarx.eclipse.devassist.ui.findings.dialogs; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Responsible for handling and formatting descriptions of scan issues. + * Provides utility methods to construct and format messages for different issue types. + * Uses HTML formatting for rich text display. + */ +public final class ProblemDescription { + + private static final String TITLE_FONT_SIZE = "font-size:11px;"; + private static final String TITLE_FONT_FAMILY = "font-family: menlo;"; + private static final String CELL_LINE_HEIGHT_STYLE = "line-height:16px;vertical-align:middle;"; + private static final String SECONDARY_SPAN_STYLE = "display:inline-block;vertical-align:middle;line-height:16px;font-size:11px;color:#ADADAD;"; + + private static final String TABLE_WITH_TR = ""; + + /** + * Formats a description for the given scan issue. + * + * @param scanIssue the ScanIssue object + * @return formatted HTML description + */ + public String formatDescription(ScanIssue scanIssue) { + StringBuilder descBuilder = new StringBuilder(); + descBuilder.append(""); + + switch (scanIssue.getScanEngine()) { + case OSS: + buildOSSDescription(descBuilder, scanIssue); + break; + case ASCA: + buildASCADescription(descBuilder, scanIssue); + break; + case SECRETS: + buildSecretsDescription(descBuilder, scanIssue); + break; + case IAC: + buildIACDescription(descBuilder, scanIssue); + break; + case CONTAINERS: + buildContainerDescription(descBuilder, scanIssue); + break; + default: + buildDefaultDescription(descBuilder, scanIssue); + } + + descBuilder.append(""); + return descBuilder.toString(); + } + + private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("
") + .append("

") + .append("").append(escapeHtml(scanIssue.getTitle())).append("@") + .append(escapeHtml(scanIssue.getPackageVersion())).append("") + .append(" - ") + .append(escapeHtml(scanIssue.getSeverity())).append(" Risk Package") + .append("

"); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + private void buildContainerDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("") + .append("

") + .append("").append(escapeHtml(scanIssue.getTitle())).append("@") + .append(escapeHtml(scanIssue.getImageTag())).append("") + .append("

"); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + private void buildIACDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + List vulnerabilities = scanIssue.getVulnerabilities(); + if (vulnerabilities != null) { + for (Vulnerability vulnerability : vulnerabilities) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("") + .append("

") + .append("").append(escapeHtml(vulnerability.getTitle())).append("") + .append(" - ").append(escapeHtml(vulnerability.getDescription())) + .append(" - IaC vulnerability") + .append("

"); + } + } + } + + private void buildSecretsDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("") + .append("

") + .append("").append(escapeHtml(formatTitle(scanIssue.getTitle()))).append("") + .append(" - Secret finding") + .append("

"); + } + + private void buildASCADescription(StringBuilder descBuilder, ScanIssue scanIssue) { + List vulnerabilities = scanIssue.getVulnerabilities(); + if (vulnerabilities != null) { + for (Vulnerability vulnerability : vulnerabilities) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("") + .append("

") + .append("").append(escapeHtml(vulnerability.getTitle())).append("") + .append(" - ").append(escapeHtml(vulnerability.getDescription())) + .append(" - SAST vulnerability") + .append("

"); + } + } + } + + private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append("
").append(escapeHtml(scanIssue.getTitle())).append(" -") + .append(escapeHtml(scanIssue.getDescription())).append("
"); + } + + 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 = vulnerabilityList.stream() + .map(Vulnerability::getSeverity) + .collect(Collectors.groupingBy(severity -> severity, Collectors.counting())); + + vulnerabilityCount.forEach((severity, count) -> { + descBuilder.append("") + .append("") + .append(count).append(""); + }); + + descBuilder.append("
"); + } + + /** + * Formats a kebab-case title into Title-Case. + */ + 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("-")); + } + + /** + * Escape HTML special characters. + */ + private String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java new file mode 100644 index 00000000..b6fa460c --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java @@ -0,0 +1,71 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.editor; +// +//import org.eclipse.jface.text.source.Annotation; +// +///** +// * Custom annotation for Findings window highlighting. +// * Represents a security finding annotation in the editor with title and description. +// */ +//public class FindingsAnnotation extends Annotation { +// +// private final String title; +// private final String description; +// +// public FindingsAnnotation(String annotationType, String title, String description) { +// super(annotationType, false, null); +// this.title = title; +// this.description = description; +// } +// +// public String getTitle() { +// return title; +// } +// +// public String getDescription() { +// return description; +// } +// +// @Override +// public String getText() { +// return title; +// } +//} +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.jface.text.source.Annotation; + +public class FindingsAnnotation extends Annotation { + + private String title; + private String description; + private List buttons = new ArrayList<>(); + + public FindingsAnnotation(String type, String title, String description) { + super(type, false, title); + this.title = title; + this.description = description; + } + + public void addButton(String label, Runnable action) { + buttons.add(new AnnotationButton(label, action)); + } + + public List getButtons() { + return buttons; + } + + public String getTitle() { return title; } + public String getDescription() { return description; } + + public static class AnnotationButton { + public String label; + public Runnable action; + + public AnnotationButton(String label, Runnable action) { + this.label = label; + this.action = action; + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java new file mode 100644 index 00000000..b1bed62e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java @@ -0,0 +1,140 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.ui.editors.text.TextEditor; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Manages highlighting and underlining of problematic code lines in the editor. + * Provides visual feedback for findings by underlining vulnerable code with severity-based colors. + * + * Supports: + * - Red wavy underline for CRITICAL/HIGH issues + * - Yellow wavy underline for MEDIUM issues + * - Blue wavy underline for LOW issues + * - Auto-clear on navigation away + */ +public class FindingsEditorOverlay { + + // These match the annotation types defined in plugin.xml + private static final String ANNOTATION_TYPE_MALICIOUS = "com.checkmarx.eclipse.findings.malicious"; + private static final String ANNOTATION_TYPE_CRITICAL = "com.checkmarx.eclipse.findings.critical"; + private static final String ANNOTATION_TYPE_HIGH = "com.checkmarx.eclipse.findings.high"; + private static final String ANNOTATION_TYPE_MEDIUM = "com.checkmarx.eclipse.findings.medium"; + private static final String ANNOTATION_TYPE_LOW = "com.checkmarx.eclipse.findings.low"; + + /** + * Highlight a problematic line in the editor. + * + * @param editor The TextEditor to highlight in + * @param issue The scan issue containing location information + */ + public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { + try { + if (editor == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + + Location location = issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // Convert to 0-based + + ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); + if (viewer == null) { + + return; + } + + IDocument document = viewer.getDocument(); + if (document == null || lineNumber < 0 || lineNumber >= document.getNumberOfLines()) { + + return; + } + + // Get line start and end offsets + int lineStartOffset = document.getLineOffset(lineNumber); + int lineLength = document.getLineLength(lineNumber); + int lineEndOffset = lineStartOffset + lineLength; + + // Create annotation for the line + String annotationType = getAnnotationTypeForSeverity(issue.getSeverity()); + FindingsAnnotation annotation = new FindingsAnnotation(annotationType, issue.getTitle(), issue.getDescription()); + Position position = new Position(lineStartOffset, lineEndOffset - lineStartOffset); + + // Add annotation to model + IAnnotationModel annotationModel = viewer.getAnnotationModel(); + if (annotationModel != null) { + annotationModel.addAnnotation(annotation, position); + + + + + + } + } catch (BadLocationException e) { + + } + } + + /** + * Clear all findings annotations from the editor. + */ + public static void clearHighlights(TextEditor editor) { + try { + if (editor == null) { + return; + } + + ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); + if (viewer == null) { + return; + } + + IAnnotationModel annotationModel = viewer.getAnnotationModel(); + if (annotationModel == null) { + return; + } + + // Remove all findings annotations + annotationModel.getAnnotationIterator().forEachRemaining(annotation -> { + if (annotation instanceof FindingsAnnotation) { + annotationModel.removeAnnotation(annotation); + } + }); + + + } catch (Exception e) { + + } + } + + /** + * Get annotation type based on severity level. + * Maps all problem severities to their corresponding annotation types. + */ + private static String getAnnotationTypeForSeverity(String severity) { + if (severity == null) { + return ANNOTATION_TYPE_MEDIUM; + } + + switch (severity.toLowerCase()) { + case "malicious": + return ANNOTATION_TYPE_MALICIOUS; + case "critical": + case "high": + return ANNOTATION_TYPE_CRITICAL; + case "medium": + return ANNOTATION_TYPE_MEDIUM; + case "low": + case "info": + return ANNOTATION_TYPE_LOW; + default: + return ANNOTATION_TYPE_MEDIUM; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java new file mode 100644 index 00000000..a5833cd1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java @@ -0,0 +1,121 @@ +package com.checkmarx.eclipse.devassist.ui.findings.icons; + +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.plugin.AbstractUIPlugin; + +import com.checkmarx.eclipse.Activator; + +/** + * Registry for managing Checkmarx severity icons. + * Handles icon loading and caching for different sizes and themes. + */ +public class IconRegistry { + + public enum Size { + SMALL("_16"), + MEDIUM("_20"); + + private final String suffix; + + Size(String suffix) { + this.suffix = suffix; + } + + public String getSuffix() { + return suffix; + } + } + + public enum Severity { + MALICIOUS("malicious"), + CRITICAL("critical"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String name; + + Severity(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + private static ImageRegistry imageRegistry; + + static { + initializeRegistry(); + } + + 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 + registerIcon("malicious", "icons/severity/malicious.svg"); + registerIcon("critical", "icons/severity/critical.svg"); + registerIcon("high", "icons/severity/high.svg"); + registerIcon("medium", "icons/severity/medium.svg"); + registerIcon("low", "icons/severity/low.svg"); + } + + private static void registerIcon(String key, String path) { + AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, path); + imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, path)); + } + + /** + * Get icon for a severity level and size. + * + * @param severity Severity level (case-insensitive) + * @param size Icon size + * @return Image instance or null if not found + */ + public static Image getIcon(String severity, Size size) { + if (severity == null) { + return null; + } + + String key = severity.toLowerCase() + size.getSuffix(); + return imageRegistry.get(key); + } + + /** + * Get icon for a severity level with default small size. + * + * @param severity Severity level + * @return Image instance or null if not found + */ + public static Image getIcon(String severity) { + return getIcon(severity, Size.SMALL); + } + + /** + * Get image registry. + * + * @return ImageRegistry instance + */ + public static ImageRegistry getRegistry() { + return imageRegistry; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java new file mode 100644 index 00000000..980648f1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java @@ -0,0 +1,243 @@ +package com.checkmarx.eclipse.devassist.ui.findings.icons; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import java.util.HashMap; +import java.util.Map; + +/** + * Composes severity icons into a single visual representation. + * Creates badges like: [C:4] [H:3] [M:1] as actual icon images + */ +public class SeverityImageComposer { + + private static final Map compositeImageCache = new HashMap<>(); + + /** + * Create a full composite image with severity icon badges displayed inline. + * Shows actual colored severity icons (🔴 🟠 🟡 🟢) after the filename. + */ + public static Image createFullCompositeImage(FileNodeLabel fileNode) { + if (fileNode == null || fileNode.getProblemCount() == null || fileNode.getProblemCount().isEmpty()) { + return null; + } + + try { + Display display = Display.getDefault(); + Image compositeImage = createFullBadgeImage(display, fileNode); + return compositeImage; + } catch (Exception e) { + return null; + } + } + + /** + * Create a composite image showing severity icons with counts inline. + * Example: Creates visual badges for Critical:4, High:3, Medium:1 + */ + public static Image createSeverityBadgeImage(FileNodeLabel fileNode) { + if (fileNode == null || fileNode.getProblemCount() == null || fileNode.getProblemCount().isEmpty()) { + return null; + } + + // Create cache key + String cacheKey = createCacheKey(fileNode); + if (compositeImageCache.containsKey(cacheKey)) { + return compositeImageCache.get(cacheKey); + } + + try { + // Get display for image creation + Display display = Display.getDefault(); + + // Create a composite image showing severity badges + // Format: Show icon + count for each severity with > 0 count + Image compositeImage = createBadgeImage(display, fileNode); + + if (compositeImage != null) { + compositeImageCache.put(cacheKey, compositeImage); + } + + return compositeImage; + } catch (Exception e) { + return null; + } + } + + /** + * Create a badge image showing severity levels inline + */ + private static Image createBadgeImage(Display display, FileNodeLabel fileNode) { + try { + // Get individual severity icons + Image criticalIcon = IconRegistry.getIcon("critical", IconRegistry.Size.SMALL); // 16x16 + Image highIcon = IconRegistry.getIcon("high", IconRegistry.Size.SMALL); + Image mediumIcon = IconRegistry.getIcon("medium", IconRegistry.Size.SMALL); + Image lowIcon = IconRegistry.getIcon("low", IconRegistry.Size.SMALL); + + // Calculate total width needed + int iconSize = 16; + int spacing = 1; + int width = 0; + + if (hasCount(fileNode, "critical")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "high")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "medium")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "low")) { + width += iconSize + spacing; + } + + if (width == 0) { + return null; + } + + // Adjust width to remove last spacing + width = Math.max(0, width - spacing); + + // Create composite image + Image compositeImage = new Image(display, width, iconSize); + GC gc = new GC(compositeImage); + gc.setBackground(display.getSystemColor(org.eclipse.swt.SWT.COLOR_WIDGET_BACKGROUND)); + gc.fillRectangle(0, 0, width, iconSize); + gc.setAntialias(org.eclipse.swt.SWT.ON); + + int x = 0; + int y = 0; + + // Draw critical icon if count > 0 + if (hasCount(fileNode, "critical") && criticalIcon != null) { + gc.drawImage(criticalIcon, x, y); + x += iconSize + spacing; + } + + // Draw high icon if count > 0 + if (hasCount(fileNode, "high") && highIcon != null) { + gc.drawImage(highIcon, x, y); + x += iconSize + spacing; + } + + // Draw medium icon if count > 0 + if (hasCount(fileNode, "medium") && mediumIcon != null) { + gc.drawImage(mediumIcon, x, y); + x += iconSize + spacing; + } + + // Draw low icon if count > 0 + if (hasCount(fileNode, "low") && lowIcon != null) { + gc.drawImage(lowIcon, x, y); + x += iconSize + spacing; + } + + gc.dispose(); + return compositeImage; + + } catch (Exception e) { + return null; + } + } + + /** + * Create a full badge image showing only severity icons inline (no text). + * Displays: [🔴][🟠][🟡][🟢] based on which severities have counts + */ + private static Image createFullBadgeImage(Display display, FileNodeLabel fileNode) { + try { + // Get individual severity icons + Image criticalIcon = IconRegistry.getIcon("critical", IconRegistry.Size.SMALL); // 16x16 + Image highIcon = IconRegistry.getIcon("high", IconRegistry.Size.SMALL); + Image mediumIcon = IconRegistry.getIcon("medium", IconRegistry.Size.SMALL); + Image lowIcon = IconRegistry.getIcon("low", IconRegistry.Size.SMALL); + + // Calculate total width needed + int iconSize = 16; + int spacing = 2; + int totalWidth = 0; + + // Count how many icons we need + int iconCount = 0; + if (hasCount(fileNode, "critical")) iconCount++; + if (hasCount(fileNode, "high")) iconCount++; + if (hasCount(fileNode, "medium")) iconCount++; + if (hasCount(fileNode, "low")) iconCount++; + + if (iconCount == 0) { + return null; + } + + // Calculate width: (iconSize + spacing) * count - spacing + totalWidth = (iconSize + spacing) * iconCount - spacing; + + // Create composite image with severity icons + Image compositeImage = new Image(display, totalWidth, iconSize); + GC gc = new GC(compositeImage); + gc.setBackground(display.getSystemColor(org.eclipse.swt.SWT.COLOR_WIDGET_BACKGROUND)); + gc.fillRectangle(0, 0, totalWidth, iconSize); + gc.setAntialias(org.eclipse.swt.SWT.ON); + + int x = 0; + int y = 0; + + // Draw critical icon + if (hasCount(fileNode, "critical") && criticalIcon != null) { + gc.drawImage(criticalIcon, x, y); + x += iconSize + spacing; + } + + // Draw high icon + if (hasCount(fileNode, "high") && highIcon != null) { + gc.drawImage(highIcon, x, y); + x += iconSize + spacing; + } + + // Draw medium icon + if (hasCount(fileNode, "medium") && mediumIcon != null) { + gc.drawImage(mediumIcon, x, y); + x += iconSize + spacing; + } + + // Draw low icon + if (hasCount(fileNode, "low") && lowIcon != null) { + gc.drawImage(lowIcon, x, y); + x += iconSize + spacing; + } + + gc.dispose(); + return compositeImage; + + } catch (Exception e) { + return null; + } + } + + private static boolean hasCount(FileNodeLabel fileNode, String severity) { + Long count = fileNode.getProblemCount().get(severity); + return count != null && count > 0; + } + + private static String createCacheKey(FileNodeLabel fileNode) { + StringBuilder key = new StringBuilder(); + key.append("c:").append(fileNode.getProblemCount().getOrDefault("critical", 0L)).append("|"); + key.append("h:").append(fileNode.getProblemCount().getOrDefault("high", 0L)).append("|"); + key.append("m:").append(fileNode.getProblemCount().getOrDefault("medium", 0L)).append("|"); + key.append("l:").append(fileNode.getProblemCount().getOrDefault("low", 0L)); + return key.toString(); + } + + public static void clearCache() { + for (Image img : compositeImageCache.values()) { + if (img != null && !img.isDisposed()) { + img.dispose(); + } + } + compositeImageCache.clear(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java new file mode 100644 index 00000000..94bd402d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java @@ -0,0 +1,212 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignored; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.runtime.preferences.ConfigurationScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.osgi.service.prefs.BackingStoreException; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Persistent storage for ignored problems. Uses Eclipse preferences to store + * ignored problem IDs. Provides thread-safe access to ignored problems list. + */ +public class IgnoredProblemsStore { + + private static final String PLUGIN_ID = "com.checkmarx.ast.eclipse"; + private static final String PREF_IGNORED_PROBLEMS = "ignoredProblems"; + private static final String SEPARATOR = ","; + + private static final IgnoredProblemsStore INSTANCE = new IgnoredProblemsStore(); + private final Set ignoredProblemIds = Collections.synchronizedSet(new HashSet<>()); + private final Map ignoredProblemsCache = Collections.synchronizedMap(new HashMap<>()); + private final List listeners = Collections.synchronizedList(new ArrayList<>()); + + private IgnoredProblemsStore() { + loadFromPreferences(); + } + + public static IgnoredProblemsStore getInstance() { + return INSTANCE; + } + + /** + * Add a problem to the ignored list (by ID only). + */ + public void ignoreProblem(String problemId) { + if (problemId != null && ignoredProblemIds.add(problemId)) { + + saveToPreferences(); + notifyListeners(); + } + } + + /** + * Add a finding to the ignored list with full finding details. + * This allows findings from the Findings View to be properly displayed in the Ignored Problems View. + */ + public void ignoreProblem(ScanIssue issue) { + if (issue != null && issue.getScanIssueId() != null) { + + ignoreProblem(issue.getScanIssueId()); + // Cache the full issue details for later retrieval + ignoredProblemsCache.put(issue.getScanIssueId(), issue); + + } else { + + } + } + + /** + * Remove a problem from the ignored list (restore it). + */ + public void restoreProblem(String problemId) { + if (problemId != null && ignoredProblemIds.remove(problemId)) { + + ignoredProblemsCache.remove(problemId); + saveToPreferences(); + notifyListeners(); + } + } + + /** + * Check if a problem is ignored. + */ + public boolean isIgnored(String problemId) { + return problemId != null && ignoredProblemIds.contains(problemId); + } + + /** + * Get all ignored problem IDs. + */ + public Set getIgnoredProblemIds() { + return new HashSet<>(ignoredProblemIds); + } + + /** + * Filter a list of issues, returning only non-ignored ones. + */ + public List filterActiveProblems(List issues) { + List active = new ArrayList<>(); + for (ScanIssue issue : issues) { + if (!isIgnored(issue.getScanIssueId())) { + active.add(issue); + } + } + return active; + } + + /** + * Get only ignored issues from a list. + */ + public List getIgnoredProblems(List allIssues) { + List ignored = new ArrayList<>(); + for (ScanIssue issue : allIssues) { + if (isIgnored(issue.getScanIssueId())) { + ignored.add(issue); + } + } + return ignored; + } + + /** + * Get all ignored issues including cached findings from the Findings View. + * Combines issues from the provided list with cached issue details. + */ + public List getAllIgnoredProblems(List allIssues) { + List result = new ArrayList<>(); + + // First add ignored issues from the provided list + if (allIssues != null) { + for (ScanIssue issue : allIssues) { + if (isIgnored(issue.getScanIssueId())) { + result.add(issue); + } + } + } + + // Then add any cached issues not yet in the result (e.g., findings from Findings View) + for (Map.Entry entry : ignoredProblemsCache.entrySet()) { + if (isIgnored(entry.getKey()) && !result.stream().anyMatch(i -> i.getScanIssueId().equals(entry.getKey()))) { + result.add(entry.getValue()); + } + } + + return result; + } + + /** + * Clear all ignored problems. + */ + public void clearAll() { + ignoredProblemIds.clear(); + ignoredProblemsCache.clear(); + saveToPreferences(); + notifyListeners(); + + } + + /** + * Register listener for ignore/restore events. + */ + public void addListener(IgnoredProblemsListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + /** + * Unregister listener. + */ + public void removeListener(IgnoredProblemsListener listener) { + listeners.remove(listener); + } + + private void notifyListeners() { + for (IgnoredProblemsListener listener : listeners) { + listener.onIgnoredProblemsChanged(); + } + } + + private void loadFromPreferences() { + try { + IEclipsePreferences prefs = ConfigurationScope.INSTANCE.getNode(PLUGIN_ID); + String ignored = prefs.get(PREF_IGNORED_PROBLEMS, ""); + if (!ignored.isEmpty()) { + String[] ids = ignored.split(SEPARATOR); + for (String id : ids) { + if (!id.trim().isEmpty()) { + ignoredProblemIds.add(id.trim()); + } + } + + } + } catch (Exception e) { + System.err.println("[IGNORED-STORE] Error loading preferences: " + e.getMessage()); + } + } + + private void saveToPreferences() { + try { + IEclipsePreferences prefs = ConfigurationScope.INSTANCE.getNode(PLUGIN_ID); + String ignored = String.join(SEPARATOR, ignoredProblemIds); + prefs.put(PREF_IGNORED_PROBLEMS, ignored); + prefs.flush(); + + } catch (BackingStoreException e) { + System.err.println("[IGNORED-STORE] Error saving preferences: " + e.getMessage()); + } + } + + public interface IgnoredProblemsListener { + void onIgnoredProblemsChanged(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java new file mode 100644 index 00000000..bb3aa862 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java @@ -0,0 +1,381 @@ +package com.checkmarx.eclipse.devassist.ui.findings.integration; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.statushandlers.StatusManager; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.IStatus; +import java.util.HashMap; +import java.util.Map; + +import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Integration with GitHub Copilot for Eclipse. + * + * Attempts to send prompts to Copilot chat via available commands. + * Falls back to clipboard if Copilot is unavailable. + * + * Implementation Strategy: + * 1. Try to find and execute Copilot commands (70% probability they exist) + * 2. If commands not available, copy prompt to clipboard + * 3. User can manually paste into Copilot Chat + * + * This ensures users always have the prompt available, either: + * - Sent automatically to Copilot (best case) + * - In clipboard for manual paste (fallback) + */ +public class CopilotIntegration { + + private static final String LOG_PREFIX = "[COPILOT-INTEGRATION]"; + + /** + * Known Copilot command IDs to try (in priority order) + * Using Microsoft Copilot for Eclipse commands + */ + private static final String COPILOT_OPEN_COMMAND = "com.microsoft.copilot.eclipse.commands.openChatView"; + private static final String COPILOT_INPUT_PARAM = "com.microsoft.copilot.eclipse.commands.openChatView.inputValue"; + private static final String COPILOT_AUTO_SEND_PARAM = "com.microsoft.copilot.eclipse.commands.openChatView.autoSend"; + + /** + * Send a prompt to Copilot Chat. + * + * Smart fallback strategy: + * 1. Try to send via Copilot command (best case) + * 2. If command fails, copy to clipboard (fallback) + * 3. Show appropriate notification based on what succeeded + * + * @param prompt The prompt to send to Copilot + * @return true if successfully sent via command or clipboard + */ + public static boolean sendPromptToCopilot(String prompt) { + if (prompt == null || prompt.isEmpty()) { + CxLogger.error("Cannot send empty prompt to Copilot", new Exception("Empty prompt")); + return false; + } + + CxLogger.info(LOG_PREFIX + " Attempting to send prompt to Copilot..."); + + // Step 1: Try to send prompt via Copilot command + boolean copilotSuccess = tryPasteAndSendToCopilot(prompt); + + if (copilotSuccess) { + CxLogger.info(LOG_PREFIX + " ✓ Successfully sent prompt via Copilot command"); + return true; + } + + // Step 2: Fallback - copy to clipboard if command failed + CxLogger.warning(LOG_PREFIX + " Copilot command failed - falling back to clipboard"); + boolean clipboardSuccess = copyToClipboard(prompt); + + if (clipboardSuccess) { + CxLogger.info(LOG_PREFIX + " ✓ Prompt copied to clipboard as fallback"); + showNotification( + "Fix Prompt Copied (Copilot Unavailable)", + "The Copilot command is not available.\n\n" + + "✓ The fix prompt has been copied to your clipboard.\n\n" + + "Open Microsoft Copilot Chat and paste (Ctrl+V) to get AI-powered fix suggestions.", + IStatus.INFO + ); + return true; // Success because clipboard worked + } + + // Both methods failed + CxLogger.error(LOG_PREFIX + " Failed to send prompt - both command and clipboard failed", + new Exception("Copilot command and clipboard fallback both failed")); + showNotification( + "Failed to Send Prompt", + "Could not send prompt to Copilot Chat or copy to clipboard.\n\n" + + "Please check that Copilot is properly installed.", + IStatus.WARNING + ); + return false; + } + + /** + * Tries to paste the prompt into Copilot Chat and trigger send + * + * @param prompt The prompt to paste + * @return true if successfully pasted, false otherwise + */ + private static boolean tryPasteAndSendToCopilot(String prompt) { + final boolean[] success = { false }; + + try { + // Step 1: Try to execute Copilot open command with prompt + CxLogger.info(LOG_PREFIX + " Attempting to execute Copilot open command with prompt..."); + boolean commandExecuted = executeOpenCopilotCommand(prompt); + if (commandExecuted) { + CxLogger.info(LOG_PREFIX + " ✓ Copilot command executed successfully - prompt sent!"); + return true; // Success - command handled it all + } else { + CxLogger.warning(LOG_PREFIX + " Copilot command execution failed - attempting manual paste fallback"); + } + + // Step 2: Try to find and paste into the view + Display.getDefault().syncExec(() -> { + try { + IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (activeWindow == null) { + CxLogger.warning(LOG_PREFIX + " No active workbench window"); + return; + } + + IWorkbenchPage activePage = activeWindow.getActivePage(); + if (activePage == null) { + CxLogger.warning(LOG_PREFIX + " No active workbench page"); + return; + } + + // Try known Copilot view IDs + String[] copilotViewIds = { + "GitHub.Copilot.Chat.View", + "com.github.copilot.chat.view", + "copilot.chatView", + "com.github.copilot.views.CopilotChatView" + }; + + IViewPart copilotView = null; + String foundViewId = null; + for (String viewId : copilotViewIds) { + try { + copilotView = activePage.findView(viewId); + if (copilotView != null) { + foundViewId = viewId; + CxLogger.info(LOG_PREFIX + " Found Copilot Chat view: " + viewId); + break; + } + } catch (Exception e) { + // Try next + } + } + + if (copilotView == null) { + CxLogger.warning(LOG_PREFIX + " Could not find Copilot Chat view after command execution"); + return; + } + + // Activate the view and bring to front + activePage.activate(copilotView); + activeWindow.getShell().forceActive(); + CxLogger.info(LOG_PREFIX + " Activated Copilot Chat view: " + foundViewId); + + // Find the text input field + Control control = copilotView.getAdapter(Control.class); + if (control == null || control.isDisposed()) { + CxLogger.warning(LOG_PREFIX + " Could not get control from Copilot view"); + return; + } + + Text inputField = findTextInputField(control); + if (inputField == null || inputField.isDisposed()) { + CxLogger.warning(LOG_PREFIX + " Could not find text input field in Copilot Chat"); + return; + } + + // Focus and set the content + inputField.setFocus(); + inputField.setText(prompt); + + CxLogger.info(LOG_PREFIX + " ✓ Successfully pasted prompt into Copilot Chat input field"); + success[0] = true; + + // Note: We don't auto-send to give user a chance to review + // User can press Enter or click Send button manually + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error pasting to Copilot Chat: " + e.getMessage()); + e.printStackTrace(); + } + }); + + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Error in tryPasteAndSendToCopilot: " + e.getMessage(), e); + } + + return success[0]; + } + + /** + * Executes Microsoft Copilot for Eclipse command to open chat with prompt + * + * @param prompt The prompt to send to Copilot + * @return true if command was executed successfully, false otherwise + */ + private static boolean executeOpenCopilotCommand(String prompt) { + final boolean[] success = { false }; + + try { + Display.getDefault().syncExec(() -> { + try { + // Get the command service + ICommandService commandService = PlatformUI.getWorkbench() + .getService(ICommandService.class); + + if (commandService == null) { + CxLogger.warning(LOG_PREFIX + " ICommandService not available"); + return; + } + + // Get the Copilot open command + Command command = commandService.getCommand(COPILOT_OPEN_COMMAND); + + if (command == null) { + CxLogger.warning(LOG_PREFIX + " Copilot command not found: " + COPILOT_OPEN_COMMAND); + return; + } + + if (!command.isEnabled()) { + CxLogger.warning(LOG_PREFIX + " Copilot command is not enabled"); + return; + } + + CxLogger.info(LOG_PREFIX + " Found Copilot command: " + COPILOT_OPEN_COMMAND); + + // Create parameters map for the command + java.util.Map parameters = new java.util.HashMap<>(); + parameters.put(COPILOT_INPUT_PARAM, prompt); + parameters.put(COPILOT_AUTO_SEND_PARAM, "true"); + + // Execute the command with parameters + try { + + command.executeWithChecks(new ExecutionEvent( + command, + parameters, + null, + null + )); + + + + + CxLogger.info(LOG_PREFIX + " ✓ Successfully executed Copilot command with prompt"); + success[0] = true; + + + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Command execution failed: " + e.getMessage()); + e.printStackTrace(); + } + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error executing Copilot command: " + e.getMessage()); + } + }); + + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Error in executeOpenCopilotCommand: " + e.getMessage(), e); + } + + return success[0]; + } + + + /** + * Recursively searches for a Text widget that appears to be the chat input (fallback) + * + * @param control The control to search + * @return The text widget if found, null otherwise + */ + private static Text findTextInputField(Control control) { + if (control == null || control.isDisposed()) { + return null; + } + + // If this is a Text widget, it might be the input field + if (control instanceof Text) { + Text text = (Text) control; + // Look for text widget that's editable and multi-line + if (!text.isDisposed() && (text.getStyle() & SWT.MULTI) != 0) { + return text; + } + } + + // Recursively search children if this is a composite + if (control instanceof Composite) { + Composite composite = (Composite) control; + Control[] children = composite.getChildren(); + for (Control child : children) { + Text found = findTextInputField(child); + if (found != null) { + return found; + } + } + } + + return null; + } + + /** + * Copies text to system clipboard + * + * @param text The text to copy + * @return true if successful, false otherwise + */ + private static boolean copyToClipboard(String text) { + try { + Display display = Display.getDefault(); + + display.syncExec(() -> { + Clipboard clipboard = new Clipboard(display); + TextTransfer transfer = TextTransfer.getInstance(); + clipboard.setContents(new Object[] { text }, new org.eclipse.swt.dnd.Transfer[] { transfer }); + clipboard.dispose(); + }); + + CxLogger.info(LOG_PREFIX + " Text copied to clipboard"); + return true; + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Failed to copy to clipboard: " + e.getMessage(), e); + return false; + } + } + + /** + * Shows a notification to the user + * + * @param title The notification title + * @param message The notification message + * @param severity The severity (IStatus.INFO, IStatus.WARNING, etc) + */ + private static void showNotification(String title, String message, int severity) { + try { + Display.getDefault().asyncExec(() -> { + IStatus status = new Status( + severity, + Activator.PLUGIN_ID, + title + "\n" + message + ); + StatusManager.getManager().handle(status, StatusManager.SHOW | StatusManager.LOG); + }); + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Failed to show notification: " + e.getMessage()); + } + } + + /** + * Checks if Copilot is available in this Eclipse instance + * Currently returns false as we use clipboard method as primary approach + * + * @return true if Copilot is available + */ + public static boolean isCopilotAvailable() { + // Clipboard method is always available as fallback + return false; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java new file mode 100644 index 00000000..b5dee657 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java @@ -0,0 +1,274 @@ +package com.checkmarx.eclipse.devassist.ui.findings.integration; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +/** + * Builds engine-specific remediation prompts for Copilot. + * + * Generates context-aware prompts based on vulnerability type: + * - ASCA (SAST): Code analysis with line numbers and remediation advice + * - OSS (Dependencies): Package information and upgrade paths + * - SECRETS: Credential type and revocation steps + * - CONTAINERS: Image information and vulnerability sources + * - IAC: Configuration details and expected vs actual values + * + * Each prompt type is optimized for the scan engine context. + */ +public class RemediationPromptBuilder { + + /** + * Build a remediation prompt for the given scan issue + * + * @param issue The scan issue to build prompt for + * @return The remediation prompt, or empty string if unable to build + */ + public static String buildRemediationPrompt(ScanIssue issue) { + if (issue == null) { + return ""; + } + + ScanEngine engine = issue.getScanEngine(); + + switch (engine) { + case ASCA: + return buildASCAPrompt(issue); + case OSS: + return buildOSSPrompt(issue); + case SECRETS: + return buildSecretPrompt(issue); + case CONTAINERS: + return buildContainerPrompt(issue); + case IAC: + return buildIACPrompt(issue); + default: + return buildGenericPrompt(issue); + } + } + + /** + * Build prompt for ASCA (SAST) vulnerabilities + * Context: Code analysis with line numbers and severity + */ + private static String buildASCAPrompt(ScanIssue issue) { + return String.format( + "You are a code security expert. Please fix the following code vulnerability:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**Line Number:** %d\n" + + "**File:** %s\n" + + "%s" + // Remediation advice if available + "\n**Requirements for the fix:**\n" + + "1. Must address the security vulnerability\n" + + "2. Should maintain code readability\n" + + "3. Must preserve existing functionality\n" + + "4. Include necessary imports/dependencies\n" + + "5. Follow Java best practices\n\n" + + "Provide the corrected code snippet.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getProblematicLineNumber(), + issue.getFilePath(), + buildRemediationAdviceSection(issue) + ); + } + + /** + * Build prompt for OSS (Open Source Software) vulnerabilities + * Context: Dependency vulnerabilities with version information + */ + private static String buildOSSPrompt(ScanIssue issue) { + return String.format( + "A vulnerability was detected in an open source dependency:\n\n" + + "**Package:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "%s" + // Version info if available + "\n**What needs to be done:**\n" + + "1. Identify recommended safe version to upgrade to\n" + + "2. Provide upgrade command (Maven/Gradle/npm/pip as applicable)\n" + + "3. List any configuration changes needed\n" + + "4. Note any compatibility concerns\n" + + "5. Suggest testing approach\n\n" + + "Please provide step-by-step remediation instructions.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + buildVersionInfo(issue) + ); + } + + /** + * Build prompt for SECRETS (credential leaks) + * Context: Exposed credentials that need immediate action + */ + private static String buildSecretPrompt(ScanIssue issue) { + return String.format( + "**SECURITY ALERT:** A secret/credential has been detected in the code:\n\n" + + "**Secret Type:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n\n" + + "**Immediate Actions Required:**\n" + + "1. If this is a real credential, immediately revoke/rotate it in the management console\n" + + "2. Generate new credentials if necessary\n" + + "3. Update application configuration to use new credentials\n" + + "4. Remove the hardcoded credential from source code\n\n" + + "**Best Practices to Prevent:**\n" + + "- Use environment variables or secrets manager\n" + + "- Never commit secrets to version control\n" + + "- Use tools like GitGuardian or TruffleHog to scan commits\n" + + "- Implement pre-commit hooks to prevent secret commits\n\n" + + "Provide detailed steps for proper secret management.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath() + ); + } + + /** + * Build prompt for CONTAINERS (container image vulnerabilities) + * Context: Docker/container image vulnerabilities + */ + private static String buildContainerPrompt(ScanIssue issue) { + return String.format( + "A vulnerability was detected in a container image:\n\n" + + "**Image/Library:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n" + + "%s" + // Version info if available + "\n**Remediation Steps:**\n" + + "1. Use specific version tags instead of 'latest'\n" + + "2. Update base image to patched version\n" + + "3. Use minimal base images (alpine, distroless)\n" + + "4. Implement scanning in CI/CD pipeline\n" + + "5. Regular image updates and patching\n\n" + + "Provide updated Dockerfile snippet and best practices.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath(), + buildVersionInfo(issue) + ); + } + + /** + * Build prompt for IAC (Infrastructure as Code) issues + * Context: Configuration and infrastructure vulnerabilities + */ + private static String buildIACPrompt(ScanIssue issue) { + return String.format( + "An infrastructure configuration vulnerability was detected:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n" + + "**Type:** Infrastructure as Code (Terraform/CloudFormation/YAML)\n" + + "%s" + // Configuration details if available + "\n**Security Requirements:**\n" + + "1. Enable encryption where applicable\n" + + "2. Enforce authentication and authorization\n" + + "3. Use least privilege principles\n" + + "4. Enable logging and monitoring\n" + + "5. Follow cloud provider security best practices\n\n" + + "Provide corrected configuration and explanation of security improvements.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath(), + buildConfigInfo(issue) + ); + } + + /** + * Generic prompt for unknown engine types + */ + private static String buildGenericPrompt(ScanIssue issue) { + return String.format( + "A security issue was detected in the code:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n\n" + + "Please provide:\n" + + "1. Root cause analysis\n" + + "2. Security implications\n" + + "3. Step-by-step fix\n" + + "4. Prevention strategies\n\n" + + "Ensure the fix follows security best practices.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath() + ); + } + + /** + * Helper: Format severity level + */ + private static String formatSeverity(String severity) { + if (severity == null) return "UNKNOWN"; + switch (severity.toUpperCase()) { + case "CRITICAL": + return "🔴 CRITICAL - Requires immediate attention"; + case "HIGH": + return "🟠 HIGH - Should be fixed soon"; + case "MEDIUM": + return "🟡 MEDIUM - Should be addressed"; + case "LOW": + return "🟢 LOW - May be fixed in next iteration"; + default: + return severity; + } + } + + /** + * Helper: Build remediation advice section if available + */ + private static String buildRemediationAdviceSection(ScanIssue issue) { + String advice = issue.getRemediationAdvise(); + if (advice != null && !advice.isEmpty()) { + return "\n**Remediation Advice:** " + advice; + } + return ""; + } + + /** + * Helper: Build version information section + */ + private static String buildVersionInfo(ScanIssue issue) { + StringBuilder sb = new StringBuilder(); + + String currentVersion = issue.getPackageVersion(); + if (currentVersion != null && !currentVersion.isEmpty()) { + sb.append("\n**Current Version:** ").append(currentVersion); + } + + // Note: Recommended version may be available from API in future versions + // For now, we rely on Copilot to suggest the safe version + + return sb.toString(); + } + + /** + * Helper: Build configuration details section + */ + private static String buildConfigInfo(ScanIssue issue) { + String description = issue.getDescription(); + if (description != null && description.length() > 100) { + return "\n**Details:** " + description; + } + return ""; + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java new file mode 100644 index 00000000..93e33774 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java @@ -0,0 +1,196 @@ +package com.checkmarx.eclipse.devassist.ui.findings.marker; + +import org.eclipse.core.resources.IMarker; + +import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Maps between ScanIssue objects and IMarker attributes. + * This is the single source of truth for marker attribute serialization. + * Allows marker resolution to reconstruct finding details without searching. + */ +public class MarkerIssueMapper { + + // 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"; + private static final String ATTR_TITLE = "cx.title"; + private static final String ATTR_DESCRIPTION = "cx.description"; + private static final String ATTR_REMEDIATION = "cx.remediation"; + private static final String ATTR_RULE_ID = "cx.ruleId"; + private static final String ATTR_FILE_PATH = "cx.filePath"; + private static final String ATTR_SCAN_ENGINE = "cx.scanEngine"; + + /** + * Reconstruct a ScanIssue from marker attributes. + * Called by marker resolution to populate the details dialog. + * + * @param marker the IMarker containing serialized issue data + * @return reconstructed ScanIssue, or null if reconstruction fails + */ + public static ScanIssue fromMarker(IMarker marker) { + try { + String issueId = marker.getAttribute(ATTR_ISSUE_ID, ""); + String severity = marker.getAttribute(ATTR_SEVERITY, "MEDIUM"); + String title = marker.getAttribute(ATTR_TITLE, marker.getAttribute(IMarker.MESSAGE, "")); + String description = marker.getAttribute(ATTR_DESCRIPTION, ""); + String remediation = marker.getAttribute(ATTR_REMEDIATION, null); + Integer ruleId = null; + try { + Object ruleIdObj = marker.getAttribute(ATTR_RULE_ID); + if (ruleIdObj instanceof Integer) { + ruleId = (Integer) ruleIdObj; + } else if (ruleIdObj instanceof String && !ruleIdObj.toString().isEmpty()) { + ruleId = Integer.parseInt(ruleIdObj.toString()); + } + } catch (Exception e) { + // Keep ruleId as null + } + String filePath = marker.getAttribute(ATTR_FILE_PATH, ""); + String scanEngineStr = marker.getAttribute(ATTR_SCAN_ENGINE, "ASCA"); + int lineNumber = marker.getAttribute(IMarker.LINE_NUMBER, 1); + int charStart = marker.getAttribute(IMarker.CHAR_START, 0); + int charEnd = marker.getAttribute(IMarker.CHAR_END, 0); + + // Reconstruct ScanIssue + ScanIssue issue = new ScanIssue(); + issue.setScanIssueId(issueId); + issue.setSeverity(severity); + issue.setTitle(title); + issue.setDescription(description); + issue.setRemediationAdvise(remediation); + issue.setRuleId(ruleId); + issue.setFilePath(filePath); + + // Parse scan engine + try { + issue.setScanEngine(ScanEngine.valueOf(scanEngineStr)); + } catch (IllegalArgumentException e) { + issue.setScanEngine(ScanEngine.ASCA); + } + + // Reconstruct location + Location location = new Location(); + location.setLine(lineNumber); + location.setStartIndex(charStart); + location.setEndIndex(charEnd); + issue.setLocations(java.util.Collections.singletonList(location)); + + return issue; + } catch (Exception e) { + + e.printStackTrace(); + return null; + } + } + + /** + * Populate marker attributes from a ScanIssue. + * Called when creating markers from findings. + * + * @param marker the IMarker to populate + * @param issue the ScanIssue containing data to serialize + */ + public static void populateMarker(IMarker marker, ScanIssue issue) { + try { + if (issue.getScanIssueId() != null && !issue.getScanIssueId().isEmpty()) { + marker.setAttribute(ATTR_ISSUE_ID, issue.getScanIssueId()); + } + + if (issue.getSeverity() != null && !issue.getSeverity().isEmpty()) { + marker.setAttribute(ATTR_SEVERITY, issue.getSeverity()); + } + + 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()); + } + + if (issue.getDescription() != null && !issue.getDescription().isEmpty()) { + marker.setAttribute(ATTR_DESCRIPTION, issue.getDescription()); + } + + if (issue.getRemediationAdvise() != null && !issue.getRemediationAdvise().isEmpty()) { + marker.setAttribute(ATTR_REMEDIATION, issue.getRemediationAdvise()); + } + + if (issue.getRuleId() != null) { + marker.setAttribute(ATTR_RULE_ID, issue.getRuleId()); + } + + if (issue.getFilePath() != null && !issue.getFilePath().isEmpty()) { + marker.setAttribute(ATTR_FILE_PATH, issue.getFilePath()); + } + + if (issue.getScanEngine() != null) { + marker.setAttribute(ATTR_SCAN_ENGINE, issue.getScanEngine().toString()); + } + + // 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()); + marker.setAttribute(IMarker.CHAR_START, location.getStartIndex()); + marker.setAttribute(IMarker.CHAR_END, location.getEndIndex()); + + // 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(); + } + } + + /** + * Convert Checkmarx severity to Eclipse marker severity level. + */ + private static int calculateMarkerSeverity(String severity) { + if (severity == null) { + return IMarker.SEVERITY_WARNING; + } + + switch (severity.toLowerCase()) { + case "critical": + case "high": + return IMarker.SEVERITY_ERROR; + case "medium": + return IMarker.SEVERITY_WARNING; + case "low": + case "info": + return IMarker.SEVERITY_INFO; + default: + return IMarker.SEVERITY_WARNING; + } + } + + /** + * Convert Checkmarx Severity enum to Eclipse marker severity level. + */ + private static int toEclipseSeverity(Severity severity) { + if (severity == null) { + return IMarker.SEVERITY_WARNING; + } + + switch (severity) { + case CRITICAL: + case HIGH: + return IMarker.SEVERITY_ERROR; + case MEDIUM: + return IMarker.SEVERITY_WARNING; + case LOW: + case INFO: + default: + return IMarker.SEVERITY_INFO; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java new file mode 100644 index 00000000..3c8cbaba --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java @@ -0,0 +1,86 @@ +package com.checkmarx.eclipse.devassist.ui.findings.model; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import org.eclipse.swt.graphics.Image; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * Represents a file node in the findings tree. + * Contains file metadata, issue counts grouped by severity, and file type icon. + * Icon is resolved at node creation time, following the JetBrains plugin pattern. + */ +public class FileNodeLabel { + + private final String fileName; + private final String filePath; + private final List issues; + private final Map problemCount; + private final Image icon; + + public FileNodeLabel(String fileName, String filePath, List issues) { + this(fileName, filePath, issues, null, null); + } + + public FileNodeLabel(String fileName, String filePath, List issues, Image icon) { + this(fileName, filePath, issues, calculateProblemCount(issues), icon); + } + + public FileNodeLabel(String fileName, String filePath, List issues, Map problemCount, Image icon) { + this.fileName = fileName; + this.filePath = filePath; + this.issues = issues; + this.problemCount = problemCount != null ? problemCount : calculateProblemCount(issues); + this.icon = icon; + } + + /** + * Calculate problem counts grouped by severity. + * Severity keys are normalized to lowercase for consistent lookups. + */ + private static Map calculateProblemCount(List issues) { + Map counts = new HashMap<>(); + + if (issues == null || issues.isEmpty()) { + return counts; + } + + for (ScanIssue issue : issues) { + String severity = issue.getSeverity(); + if (severity != null) { + // Normalize severity to lowercase for consistent map keys + String normalizedSeverity = severity.toLowerCase(); + counts.put(normalizedSeverity, counts.getOrDefault(normalizedSeverity, 0L) + 1); + } + } + + return counts; + } + + public String getFileName() { + return fileName; + } + + public String getFilePath() { + return filePath; + } + + public List getIssues() { + return issues; + } + + public Map getProblemCount() { + return problemCount; + } + + public Image getIcon() { + return icon; + } + + @Override + public String toString() { + return fileName; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java new file mode 100644 index 00000000..b0c4be77 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java @@ -0,0 +1,31 @@ +package com.checkmarx.eclipse.devassist.ui.findings.model; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Represents a scan issue with its associated file path. + * Used as a leaf node in the findings tree. + */ +public class ScanDetailWithPath { + + private final ScanIssue detail; + private final String filePath; + + public ScanDetailWithPath(ScanIssue detail, String filePath) { + this.detail = detail; + this.filePath = filePath; + } + + public ScanIssue getDetail() { + return detail; + } + + public String getFilePath() { + return filePath; + } + + @Override + public String toString() { + return detail != null ? detail.getTitle() : "Unknown"; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java new file mode 100644 index 00000000..7eaa61de --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java @@ -0,0 +1,112 @@ +package com.checkmarx.eclipse.devassist.ui.findings.provider; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.IEditorRegistry; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; + +import java.util.List; +import java.util.Map; + +/** + * Content provider for the Findings tree viewer. + * Implements {@link ITreeContentProvider} to provide hierarchical content structure. + * Organizes scan issues by file path as parent nodes with individual issues as children. + */ +public class FindingsContentProvider implements ITreeContentProvider { + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @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) { + Image image = imageDescriptor.createImage(); + if (image != null) { + return image; + } + } + } catch (Exception 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())) + .toArray(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof ScanDetailWithPath) { + // Parent is the file node - would need to track in the model + return null; + } + 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() { + // Cleanup if needed + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java new file mode 100644 index 00000000..0d462a32 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java @@ -0,0 +1,164 @@ +package com.checkmarx.eclipse.devassist.ui.findings.provider; + +import java.util.Map; +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.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.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; + +/** + * Label provider tailored exactly to render severity shield badges + * sequentially to the right of file labels. + */ +public class FindingsLabelProvider extends DelegatingStyledCellLabelProvider { + + private static final String[] SEVERITIES = { "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 + + public FindingsLabelProvider() { + super(new IStyledLabelProvider() { + @Override + public StyledString getStyledText(Object element) { + if (element instanceof FileNodeLabel) { + return new StyledString(((FileNodeLabel) element).getFileName()); + } else if (element instanceof ScanDetailWithPath) { + return new StyledString(formatIssueText(((ScanDetailWithPath) element).getDetail())); + } + return new StyledString(element.toString()); + } + + @Override + public Image getImage(Object element) { + if (element instanceof FileNodeLabel) { + return ((FileNodeLabel) element).getIcon(); + } else if (element instanceof ScanDetailWithPath) { + String severity = ((ScanDetailWithPath) element).getDetail().getSeverity(); + return IconRegistry.getIcon(severity, IconRegistry.Size.SMALL); + } + return null; + } + + @Override public void dispose() {} + @Override public void addListener(ILabelProviderListener l) {} + @Override public void removeListener(ILabelProviderListener l) {} + @Override public boolean isLabelProperty(Object el, String prop) { return false; } + + private String formatIssueText(ScanIssue detail) { + switch (detail.getScanEngine()) { + case OSS: return detail.getSeverity() + "-risk package: " + detail.getTitle() + "@" + detail.getPackageVersion() + getLineNumberText(detail); + case SECRETS: return detail.getSeverity() + "-risk secret: " + detail.getTitle() + getLineNumberText(detail); + case CONTAINERS: return detail.getSeverity() + "-risk container image: " + detail.getTitle() + ":" + detail.getImageTag() + getLineNumberText(detail); + case ASCA: + case IAC: return detail.getTitle() + getLineNumberText(detail); + default: return detail.getDescription() + getLineNumberText(detail); + } + } + + private String getLineNumberText(ScanIssue detail) { + if (detail.getLocations() != null && !detail.getLocations().isEmpty()) { + return " [Ln " + detail.getLocations().get(0).getLine() + ", Col " + detail.getLocations().get(0).getStartIndex() + "]"; + } + return ""; + } + }); + } + + @Override + protected void measure(Event event, Object element) { + super.measure(event, element); + + if (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) element; + Map counts = fileNode.getProblemCount(); + + if (counts != null && !counts.isEmpty()) { + int extraWidth = TEXT_TO_BADGE_PADDING; + for (String severity : SEVERITIES) { + if (counts.containsKey(severity) && counts.get(severity) > 0) { + String countStr = String.valueOf(counts.get(severity)); + int textWidth = event.gc.textExtent(countStr).x; + // 16px (Icon) + 4px (Gap between icon & number) + number length + gap to next badge + extraWidth += 16 + 0 + textWidth + BETWEEN_BADGE_SPACING; + } + } + event.width += extraWidth; + } + } + } + + @Override + protected void paint(Event event, Object element) { + // 1. Draw standard tree node elements (Expand/collapse arrows, file icons, text strings) + super.paint(event, element); + + // 2. Lay down the right-aligned badges + if (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) element; + Map counts = fileNode.getProblemCount(); + + if (counts != null && !counts.isEmpty()) { + // Determine exactly where the file label ends horizontally + Point textSize = event.gc.textExtent(fileNode.getFileName()); + + // Base offset: layout context starting position + text length + margin padding + int currentX = event.x + textSize.x + TEXT_TO_BADGE_PADDING; + + int rowHeight = event.height; + int iconY = event.y + (rowHeight - 16) / 2; + int textY = event.y + (rowHeight - event.gc.getFontMetrics().getHeight()) / 2; + + 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); + + if (badgePng != null) { + // Draw Shield Badge + event.gc.drawImage(badgePng, currentX, iconY); + currentX += 16 + 4; // Shift right right past shield + a tiny gap + + // 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) + 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)); + } + + // 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) { + fd.setStyle(fd.getStyle() | SWT.BOLD); + } + org.eclipse.swt.graphics.Font boldFont = new org.eclipse.swt.graphics.Font(event.display, fontData); + event.gc.setFont(boldFont); + + event.gc.drawString(countStr, currentX, textY, true); + + // Restore original font + event.gc.setFont(originalFont); + boldFont.dispose(); + + // Advance cursor layout pointer to the next shield group block + currentX += event.gc.textExtent(countStr).x + BETWEEN_BADGE_SPACING; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java new file mode 100644 index 00000000..13d1a362 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java @@ -0,0 +1,108 @@ +package com.checkmarx.eclipse.devassist.ui.findings.realtime; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java new file mode 100644 index 00000000..6fd14af3 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java new file mode 100644 index 00000000..64e95904 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/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.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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java new file mode 100644 index 00000000..31584f54 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java @@ -0,0 +1,26 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolutionGenerator2; + +/** + * Provides marker resolutions for Checkmarx findings. + * 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 { + + @Override + public IMarkerResolution[] getResolutions(IMarker marker) { + return new IMarkerResolution[] { + new ViewFindingDetailsResolution(marker) + }; + } + + @Override + public boolean hasResolutions(IMarker marker) { + // We always provide the "View Finding Details" resolution + return true; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java new file mode 100644 index 00000000..7881693f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java @@ -0,0 +1,258 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * 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. + */ +public class ViewFindingDetailsResolution implements IMarkerResolution2 { + + public ViewFindingDetailsResolution(IMarker marker) { + // Constructor parameter kept for instantiation, marker details retrieved from run() parameter + } + + @Override + public String getLabel() { + return "View Finding Details"; + } + + @Override + public String getDescription() { + return "Open detailed information about this Checkmarx finding"; + } + + @Override + public Image getImage() { + // Optional: Return an icon. For now, use default + return null; + } + + @Override + public void run(IMarker marker) { + try { + // 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 + ); + dialog.open(); + + + + } catch (Exception e) { + + e.printStackTrace(); + } + } + + /** + * Simple dialog that displays finding details. + * Reuses the UI structure from FindingsInformationControl. + */ + private static class FindingDetailsDialog extends Dialog { + + private ScanIssue issue; + + public FindingDetailsDialog(Shell parentShell, ScanIssue issue) { + super(parentShell); + this.issue = issue; + setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Checkmarx Finding Details - " + (issue.getTitle() != null ? issue.getTitle() : "")); + newShell.setSize(500, 400); + + // Center on screen + Shell parent = getParentShell(); + if (parent != null) { + org.eclipse.swt.graphics.Rectangle bounds = parent.getBounds(); + Point size = newShell.getSize(); + newShell.setLocation( + bounds.x + (bounds.width - size.x) / 2, + bounds.y + (bounds.height - size.y) / 2 + ); + } + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + container.setLayout(new GridLayout(1, false)); + + // Severity label with icon + Label severityLabel = new Label(container, SWT.NONE); + severityLabel.setText(getSeverityIcon(issue.getSeverity()) + " " + getSeverityText(issue.getSeverity())); + severityLabel.setFont(container.getDisplay().getSystemFont()); + GridData gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + severityLabel.setLayoutData(gd); + + // Title label + Label titleLabel = new Label(container, SWT.WRAP); + titleLabel.setText("Title: " + (issue.getTitle() != null ? issue.getTitle() : "")); + gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + gd.widthHint = 480; + titleLabel.setLayoutData(gd); + + // Description text (scrollable) + Text descriptionText = new Text(container, SWT.WRAP | SWT.READ_ONLY | SWT.V_SCROLL | SWT.BORDER); + descriptionText.setText(issue.getDescription() != null ? issue.getDescription() : ""); + gd = new GridData(SWT.FILL, SWT.FILL, true, true); + gd.heightHint = 120; + gd.widthHint = 480; + descriptionText.setLayoutData(gd); + + // Remediation advice (if available) + if (issue.getRemediationAdvise() != null && !issue.getRemediationAdvise().isEmpty()) { + Label remediationLabel = new Label(container, SWT.WRAP); + remediationLabel.setText("Remediation: " + issue.getRemediationAdvise()); + gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + gd.widthHint = 480; + remediationLabel.setLayoutData(gd); + } + + // Buttons composite + Composite buttonsComposite = new Composite(container, SWT.NONE); + buttonsComposite.setLayout(new GridLayout(4, true)); + gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + buttonsComposite.setLayoutData(gd); + + // Quick Fix button + Button quickFixBtn = new Button(buttonsComposite, SWT.PUSH); + quickFixBtn.setText("⚡ Quick Fix"); + quickFixBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + quickFixBtn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onQuickFixClick(); + } + }); + + // Ignore button + Button ignoreBtn = new Button(buttonsComposite, SWT.PUSH); + ignoreBtn.setText("🚫 Ignore"); + ignoreBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + ignoreBtn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onIgnoreClick(); + } + }); + + // Copy button + Button copyBtn = new Button(buttonsComposite, SWT.PUSH); + copyBtn.setText("📋 Copy"); + copyBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + copyBtn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onCopyClick(); + } + }); + + // Open Window button + Button openBtn = new Button(buttonsComposite, SWT.PUSH); + openBtn.setText("🪟 Details"); + openBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + openBtn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onOpenWindowClick(); + } + }); + + return container; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + // Remove default OK/Cancel buttons, add Close button + createButton(parent, org.eclipse.jface.dialogs.IDialogConstants.CLOSE_ID, "Close", true); + } + + private void onQuickFixClick() { + + // TODO: Implement remediation integration + } + + private void onIgnoreClick() { + + // TODO: Implement ignore logic + } + + private void onCopyClick() { + String title = issue.getTitle() != null ? issue.getTitle() : ""; + String description = issue.getDescription() != null ? issue.getDescription() : ""; + String text = title + "\n" + description; + + getShell().getDisplay().asyncExec(() -> { + Clipboard clipboard = new Clipboard(getShell().getDisplay()); + 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 + } + + private String getSeverityIcon(String severity) { + if (severity == null) { + return "⚪"; + } + switch (severity.toLowerCase()) { + case "critical": + return "🔴"; + case "high": + return "🟠"; + case "medium": + return "🟡"; + case "low": + return "🟢"; + default: + return "⚪"; + } + } + + private String getSeverityText(String severity) { + return severity != null ? severity.toUpperCase() : "UNKNOWN"; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java new file mode 100644 index 00000000..486a986e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java @@ -0,0 +1,94 @@ +package com.checkmarx.eclipse.devassist.ui.findings.utils; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +import java.util.Arrays; +import java.util.List; + +/** + * Utility methods for findings view. + */ +public class FindingsUtils { + + private static final List SEVERITY_ORDER = Arrays.asList( + "malicious", "critical", "high", "medium", "low"); + + /** + * Check if a severity level represents a problem. + * + * @param severity Severity level + * @return true if severity is a problem level + */ + public static boolean isProblem(String severity) { + if (severity == null) { + return false; + } + String lower = severity.toLowerCase(); + return lower.equals("malicious") || lower.equals("critical") + || lower.equals("high") || lower.equals("medium") || lower.equals("low"); + } + + /** + * Get severity order priority (lower number = higher severity). + * + * @param severity Severity level + * @return Priority index (0 = highest) + */ + public static int getSeverityPriority(String severity) { + if (severity == null) { + return Integer.MAX_VALUE; + } + int index = SEVERITY_ORDER.indexOf(severity.toLowerCase()); + return index >= 0 ? index : Integer.MAX_VALUE; + } + + /** + * Get formatted issue text based on scan engine type. + * + * @param issue Scan issue + * @return Formatted text + */ + public static String getFormattedIssueText(ScanIssue issue) { + if (issue == null) { + return ""; + } + + ScanEngine engine = issue.getScanEngine(); + if (engine == null) { + return issue.getDescription(); + } + + switch (engine) { + case OSS: + return issue.getSeverity() + "-risk package: " + issue.getTitle() + "@" + issue.getPackageVersion(); + case SECRETS: + return issue.getSeverity() + "-risk secret: " + issue.getTitle(); + case CONTAINERS: + return issue.getSeverity() + "-risk container image: " + issue.getTitle() + ":" + issue.getImageTag(); + case ASCA: + case IAC: + return issue.getTitle(); + default: + return issue.getDescription(); + } + } + + /** + * Extract file name from full path. + * + * @param filePath Full file path + * @return File name + */ + public static 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; + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java new file mode 100644 index 00000000..30ca3d16 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java @@ -0,0 +1,159 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.util.List; + +/** + * The DevAssistConstants class defines a collection of constant values + * related to real-time scanning functionalities, including support for + * different scanning engines and associated configurations. + */ +public final class DevAssistConstants { + + private DevAssistConstants() { + throw new UnsupportedOperationException("Cannot instantiate DevAssistConstants class"); + } + + // Tab Name Constants + public static final String DEVASSIST_TAB = "Checkmarx One Assist Findings"; + public static final String IGNORED_FINDINGS_TAB = "Ignored Findings"; + public static final String DEVASSIST_PLUGIN_FINDINGS_WINDOW_NAME = "Checkmarx Developer Assist Findings"; + + // OSS Scanner Constants + public static final String ACTIVATE_OSS_REALTIME_SCANNER = "Activate OSS-Realtime"; + public static final String OSS_REALTIME_SCANNER = "Checkmarx Open Source Realtime Scanner (OSS-Realtime)"; + public static final String OSS_REALTIME_SCANNER_START = "Realtime OSS Scanner Engine started"; + public static final String OSS_REALTIME_SCANNER_DISABLED = "Realtime OSS Scanner Engine disabled"; + public static final String OSS_REALTIME_SCANNER_DIRECTORY = "Cx-oss-realtime-scanner"; + public static final String ERROR_OSS_REALTIME_SCANNER = "Failed to handle OSS Realtime scan"; + + // Container Scanner Constants + public static final String ACTIVATE_CONTAINER_REALTIME_SCANNER = "Activate Containers-Realtime"; + public static final String CONTAINER_REALTIME_SCANNER = "Checkmarx Containers Realtime Scanner (Containers-Realtime)"; + public static final String CONTAINER_REALTIME_SCANNER_START = "Realtime Containers Scanner Engine started"; + public static final String CONTAINER_REALTIME_SCANNER_DISABLED = "Realtime Containers Scanner Engine disabled"; + public static final String CONTAINER_REALTIME_SCANNER_DIRECTORY = "Cx-containers-realtime-scanner"; + public static final String ERROR_CONTAINER_REALTIME_SCANNER = "Failed to handle Containers Realtime scan"; + + // Secrets Scanner Constants + public static final String ACTIVATE_SECRETS_REALTIME_SCANNER = "Activate Secrets-Realtime"; + public static final String SECRETS_REALTIME_SCANNER = "Checkmarx Secrets Realtime Scanner (Secrets-Realtime)"; + public static final String SECRETS_REALTIME_SCANNER_START = "Realtime Secrets Scanner Engine started"; + public static final String SECRETS_REALTIME_SCANNER_DISABLED = "Realtime Secrets Scanner Engine disabled"; + public static final String SECRETS_REALTIME_SCANNER_DIRECTORY = "Cx-secrets-realtime-scanner"; + public static final String ERROR_SECRETS_REALTIME_SCANNER = "Failed to handle Secrets Realtime scan"; + + // IaC Scanner Constants + public static final String ACTIVATE_IAC_REALTIME_SCANNER = "Activate IAC-Realtime"; + public static final String IAC_REALTIME_SCANNER = "Checkmarx IAC Realtime Scanner (IAC-Realtime)"; + public static final String IAC_REALTIME_SCANNER_START = "Realtime IAC Scanner Engine started"; + public static final String IAC_REALTIME_SCANNER_DISABLED = "Realtime IAC Scanner Engine disabled"; + public static final String IAC_REALTIME_SCANNER_DIRECTORY = "Cx-iac-realtime-scanner"; + public static final String ERROR_IAC_REALTIME_SCANNER = "Failed to handle IAC Realtime scan"; + public static final String IAC_PREREQUISITE = "Please refer IAC RealTime Scanner Prerequisites"; + public static final String IAC_ENGINE_VALIDATION_ERROR = "Checkmarx Containers Management Tool Error"; + + // ASCA Scanner Constants + public static final String ACTIVATE_ASCA_REALTIME_SCANNER = "Activate ASCA-Realtime"; + public static final String ASCA_REALTIME_SCANNER = "Checkmarx AI Secure Coding Assistant (ASCA)"; + public static final String ASCA_REALTIME_SCANNER_START = "AI Secure Coding Assistant Engine started."; + public static final String ASCA_REALTIME_SCANNER_DISABLED = "AI Secure Coding Assistant Engine disabled."; + public static final String ERROR_ASCA_REALTIME_SCANNER = "Failed to handle ASCA Realtime scan"; + + // ASCA Supported File Extensions + public static final List ASCA_SUPPORTED_EXTENSIONS = List.of( + "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"; + public static final String FIX_WITH_DEV_ASSIST = "Fix with Checkmarx Developer Assist"; + 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"; + + // Manifest file patterns + public static final List MANIFEST_FILE_PATTERNS = List.of( + "**/Directory.Packages.props", + "**/packages.config", + "**/pom.xml", + "**/package.json", + "**/requirements.txt", + "**/go.mod", + "**/*.csproj", + "**/build.gradle", + "**/build.gradle.kts", + "**/yarn.lock", + "**/*.sbt", + "**/Gemfile", + "**/bower.json", + "**/requirement-*.txt", + "**/requirements-*.txt", + "**/Setup.py", + "**/Setup.cfg", + "**/pyproject.toml", + "**/poetry.lock", + "**/Package.swift", + "**/Package.resolved", + "**/composer.json", + "**/composer.lock", + "**/*.podspec.json", + "**/*.podspec", + "**/Podfile", + "**/Podfile.lock", + "**/Cartfile.resolved", + "**/Gemfile.lock", + "**/cpanfile.snapshot", + "**/cpanfile", + "**/pubspec.lock" + ); + + // Container file patterns + public static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile", + "**/dockerfile-*", + "**/dockerfile.*", + "**/docker-compose.yml", + "**/docker-compose.yaml", + "**/docker-compose-*.yml", + "**/docker-compose-*.yaml" + ); + + // IaC file patterns and extensions + public static final List IAC_SUPPORTED_PATTERNS = List.of( + "**/dockerfile", + "**/*.auto.tfvars", + "**/*.terraform.tfvars" + ); + + public static final List IAC_FILE_EXTENSIONS = List.of( + "tf", "yaml", "yml", "json", "proto", "dockerfile" + ); + + // Multiple issues on same line + public static final String MULTIPLE_IAC_ISSUES = " IAC issues detected on this line"; + public static final String MULTIPLE_ASCA_ISSUES = " ASCA violations detected on this line"; + + // Container file types + public static final String DOCKERFILE = "dockerfile"; + public static final String DOCKER_COMPOSE = "docker-compose"; + public static final String HELM = "helm"; + public static final List CONTAINER_HELM_EXTENSION = List.of("yml", "yaml"); + public static final List CONTAINER_HELM_EXCLUDED_FILES = List.of("chart.yml", "chart.yaml"); + + // Container image risk descriptions + public static final String MALICIOUS_RISK_CONTAINER = "Malicious-risk container image"; + public static final String CRITICAL_RISK_CONTAINER = "Critical-risk container image"; + public static final String HIGH_RISK_CONTAINER = "High-risk container image"; + public static final String MEDIUM_RISK_CONTAINER = "Medium-risk container image"; + public static final String LOW_RISK_CONTAINER = "Low-risk container image"; + + // General constants + public static final String SEVERITY_PACKAGE = "Severity Package"; + public static final String THEME = "THEME"; + 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 QUICK_FIX = "QUICK_FIX"; + public static final String UNDO = "Undo"; +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java new file mode 100644 index 00000000..8e04fc14 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -0,0 +1,258 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +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.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Utility class for DevAssist operations. Provides methods for encoding, decoding, + * severity normalization, and file type detection. + */ +public class DevAssistUtils { + private static final String LOG_TAG = "[DEV-ASSIST-UTILS]"; + + public static final String DOCKERFILE = "dockerfile"; + public static final String DOCKER_COMPOSE = "docker-compose"; + public static final String HELM = "helm"; + + private DevAssistUtils() { + // Private constructor to prevent instantiation + } + + /** + * Generate a unique ID for scan issue based on line, rule info, and file name. + * Mirrors JetBrains pattern: base64(line + ruleInfo + fileName) + * + * @param line Line number where issue occurs + * @param ruleInfo Rule ID + Rule Name concatenated + * @param fileName Name of the file (not full path, just filename) + * @return Deterministic base64-encoded ID + */ + public static String generateUniqueId(int line, String ruleInfo, String fileName) { + String input = line + "|" + ruleInfo + "|" + fileName; + return encodeBase64(input); + } + + /** + * Encode the input string using Base64. Uses UTF-8 encoding to match JetBrains + * implementation. + * + * @param input String to be encoded + * @return Base64 encoded string + */ + public static String encodeBase64(String input) { + if (input == null || input.isEmpty()) { + CxLogger.warning(LOG_TAG + " Attempting to encode null or empty string"); + return ""; + } + try { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error encoding string to Base64: " + e.getMessage(), e); + return ""; + } + } + + /** + * Decode a Base64 string back to its original form. Used for debugging or ID + * verification. + * + * @param encoded Base64 encoded string + * @return Decoded string + */ + public static String decodeBase64(String encoded) { + if (encoded == null || encoded.isEmpty()) { + return ""; + } + try { + return new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error decoding Base64 string: " + e.getMessage()); + return ""; + } + } + + /** + * Normalize severity string to match SeverityLevel enum format (capitalized). + * + * @param severity Raw severity string from API + * @return Normalized severity in SeverityLevel format, or original if no match + */ + public static String normalizeSeverity(String severity) { + if (severity == null || severity.isEmpty()) { + return "Unknown"; + } + 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; + } + } + + /** + * Check if severity represents a problem (displayable finding). + * + * @param severity Severity string (case-insensitive) + * @return true if severity is a problem, false if OK/UNKNOWN/IGNORED + */ + public static boolean isProblem(String severity) { + if (severity == null) { + return false; + } + return !severity.equalsIgnoreCase(SeverityLevel.OK.getSeverity()) + && !severity.equalsIgnoreCase(SeverityLevel.UNKNOWN.getSeverity()) + && !severity.equalsIgnoreCase(SeverityLevel.IGNORED.getSeverity()); + } + + /** + * Check if the given file path corresponds to a Docker Compose file. + * + * @param filePath Full path to the file + * @return true if it's a Docker Compose file, false otherwise + */ + public static boolean isDockerComposeFile(@NonNull String filePath) { + return Paths.get(filePath).getFileName().toString().toLowerCase().contains("docker-compose"); + } + + /** + * Check if the given file path corresponds to a Dockerfile. + * + * @param filePath Full path to the file + * @return true if it's a Dockerfile, false otherwise + */ + public static boolean isDockerFile(@NonNull String filePath) { + return Paths.get(filePath).getFileName().toString().toLowerCase().contains("dockerfile"); + } + + /** + * Check if the given file path is a YAML file. + * + * @param filePath Full path to the file + * @return true if it's a YAML file, false otherwise + */ + public static boolean isYamlFile(String filePath) { + if (Objects.isNull(filePath) || filePath.isBlank()) { + return false; + } + String fileExtension = getFileExtension(filePath); + return Objects.nonNull(fileExtension) + && 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 + */ + public static String getFileExtension(String filePath) { + if (filePath == null || filePath.isBlank()) { + return null; + } + int lastDot = filePath.lastIndexOf('.'); + int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + + if (lastDot > lastSeparator && lastDot < filePath.length() - 1) { + return filePath.substring(lastDot + 1).toLowerCase(); + } + return null; + } + + /** + * 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 + * 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 + * from a background Job thread and Workbench/editor APIs are not thread-safe. + * + * @param filePath Absolute OS file path to look up + * @return the live IDocument if the file is open in a text editor, else null + */ + public static IDocument getLiveDocumentForFile(String filePath) { + if (filePath == null || filePath.isBlank()) { + return null; + } + + final IDocument[] result = new IDocument[1]; + try { + Display display = Display.getDefault(); + if (display == null || display.isDisposed()) { + return null; + } + + display.syncExec(() -> { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null || workbench.isClosing()) { + return; + } + for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) { + for (IWorkbenchPage page : window.getPages()) { + for (IEditorReference ref : page.getEditorReferences()) { + IEditorPart editor = ref.getEditor(false); + if (!(editor instanceof ITextEditor)) { + continue; + } + ITextEditor textEditor = (ITextEditor) editor; + try { + IFile file = textEditor.getEditorInput().getAdapter(IFile.class); + if (file != null && file.getLocation() != null + && file.getLocation().toOSString().equals(filePath)) { + result[0] = textEditor.getDocumentProvider() + .getDocument(textEditor.getEditorInput()); + return; + } + } catch (Exception e) { + // Skip editors we can't inspect + } + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error resolving live document for: " + filePath + " - " + e.getMessage()); + } + }); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error in getLiveDocumentForFile: " + e.getMessage()); + } + + return result[0]; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java new file mode 100644 index 00000000..26086213 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/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 +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java index e520f992..2eac1a86 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java @@ -7,10 +7,16 @@ import org.eclipse.ui.PlatformUI; import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxEditorListener; +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.listener.ProjectLifecycleListener; public class PluginStartup implements IStartup { private static final String VIEW_ID = "com.checkmarx.eclipse.views.CheckmarxView"; + private static final String FINDINGS_VIEW_ID = "com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView"; + private static CheckmarxEditorListener realtimeScanListener; // Keep strong reference to prevent GC + private static ProjectLifecycleListener projectListener; // Keep strong reference to prevent GC @Override public void earlyStartup() { @@ -19,13 +25,50 @@ public void earlyStartup() { IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null) { IWorkbenchPage page = window.getActivePage(); + + // Show Checkmarx One view if not already visible if (page != null && page.findView(VIEW_ID) == null) { page.showView(VIEW_ID); } + + // Show Checkmarx Findings view if not already visible + if (page != null && page.findView(FINDINGS_VIEW_ID) == null) { + page.showView(FINDINGS_VIEW_ID); + } + + // Register listener for real-time scanning with debounce + realtimeScanListener = new CheckmarxEditorListener(); + window.getPartService().addPartListener(realtimeScanListener); + + // Initialize backend scanner infrastructure + initializeBackendScanners(); } } catch (PartInitException e) { - CxLogger.error("Failed to open Checkmarx One view on startup: " + e.getMessage(), e); + CxLogger.error("Failed to open Checkmarx views on startup: " + e.getMessage(), e); + } catch (Exception e) { + CxLogger.error("Error during plugin startup: " + e.getMessage(), e); } }); } + + /** + * Initialize backend scanner infrastructure. + * + * Creates and registers: + * - GlobalScannerController (application-level singleton) + * - ProjectLifecycleListener (project open/close listener) + * + * This enables real-time scanning on file modifications. + */ + private void initializeBackendScanners() { + try { + GlobalScannerController controller = GlobalScannerController.getInstance(); + CxLogger.info(controller.getStateReport()); + + projectListener = new ProjectLifecycleListener(); + projectListener.register(); + } catch (Exception e) { + CxLogger.error("Error initializing backend scanners: " + e.getMessage(), e); + } + } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java index 524b1136..6a37b498 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java @@ -21,6 +21,7 @@ public class PluginConstants { public static final String BFL_NOT_FOUND = "Best fix Location not available for given results"; public static final String TOOLBAR_ACTION_PREFERENCES = "Preferences"; public static final String TOOLBAR_ACTION_CLEAR_RESULTS = "Clear results section"; + public static final String FINDINGS_PROMO_DESCRIPTION = "Checkmarx AI (Cx Assist) provides real-time threat detection and helps you avoid vulnerabilities before they happen."; /******************************** LOG VIEW: ERRORS ********************************/ diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java index 50f6ff23..3ccf556e 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java @@ -52,7 +52,7 @@ public static String convertStringTimeStamp(String timestamp) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN).withZone(ZoneId.systemDefault()); parsedDate = dateTimeFormatter.format(instant); } catch (Exception e) { - System.out.println(e); + return timestamp; } diff --git a/pom.xml b/pom.xml index ddc93a88..a1a45693 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,8 @@ + +