diff --git a/.gitattributes b/.gitattributes index b9b6fd84..d1213751 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,4 @@ checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.20.jar filter=lfs diff checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.21.jar filter=lfs diff=lfs merge=lfs -text checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.23.jar filter=lfs diff=lfs merge=lfs -text checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.24.jar filter=lfs diff=lfs merge=lfs -text +devassist-lib/lib/*.jar filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index f560f662..767b2e32 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .vs/ *.jar !checkmarx-ast-eclipse-plugin/lib/*.jar +!devassist-lib/lib/*.jar diff --git a/JETBRAINS_SCANNER_STATE_MANAGEMENT.md b/JETBRAINS_SCANNER_STATE_MANAGEMENT.md new file mode 100644 index 00000000..ea679281 --- /dev/null +++ b/JETBRAINS_SCANNER_STATE_MANAGEMENT.md @@ -0,0 +1,527 @@ +--- +name: jetbrains-scanner-state-management +description: "Complete scanner enable/disable state management from JetBrains plugin - authentication, user preferences, and execution logic" +metadata: + node_type: memory + type: reference + originSessionId: 038b3b60-f0cb-4518-94b9-500be45087d9 + modified: 2026-08-09T15:20:04.349Z +--- + +# JetBrains Scanner State Management - Complete Reference + +## Overview +The JetBrains plugin implements sophisticated state management for real-time scanners (ASCA, OSS, Secrets, Containers, IaC) with three key layers: +1. **Persistent State** - Saved to disk, survives IDE restarts +2. **User Preferences** - Preserved when features toggle (MCP on/off) +3. **Runtime Execution** - Determines which scanners actually run on code + +--- + +## Layer 1: Persistent State (GlobalSettingsState) + +### Current Scanner States +```java +// Current enablement status +private boolean ascaRealtime = false; +private boolean ossRealtime = false; +private boolean secretDetectionRealtime = false; +private boolean containersRealtime = false; +private boolean iacRealtime = false; +private String containersTool = "docker"; +``` + +### User Preferences (Persisted) +```java +// User's custom choices - preserved even when features are disabled +@Attribute("userPreferencesSet") +private boolean userPreferencesSet = false; + +@Attribute("userPrefAscaRealtime") +private boolean userPrefAscaRealtime = false; + +@Attribute("userPrefOssRealtime") +private boolean userPrefOssRealtime = false; + +@Attribute("userPrefSecretDetectionRealtime") +private boolean userPrefSecretDetectionRealtime = false; + +@Attribute("userPrefContainersRealtime") +private boolean userPrefContainersRealtime = false; + +@Attribute("userPrefIacRealtime") +private boolean userPrefIacRealtime = false; +``` + +### MCP Status Flags +```java +@Attribute("mcpEnabled") +private boolean mcpEnabled = false; + +@Attribute("mcpStatusChecked") +private boolean mcpStatusChecked = false; + +// License tracking +@Attribute("isDevAssistLicenseEnabled") +private boolean isDevAssistLicenseEnabled = false; + +@Attribute("isOneAssistLicenseEnabled") +private boolean isOneAssistLicenseEnabled = false; +``` + +--- + +## Layer 2: User Preference Methods + +### Save Preferences (Before Disabling Scanners) +```java +public void saveCurrentSettingsAsUserPreferences() { + setUserPreferences(ascaRealtime, ossRealtime, secretDetectionRealtime, + containersRealtime, iacRealtime); +} + +public void setUserPreferences(boolean ascaRealtime, boolean ossRealtime, + boolean secretDetectionRealtime, + boolean containersRealtime, boolean iacRealtime) { + this.userPrefAscaRealtime = ascaRealtime; + this.userPrefOssRealtime = ossRealtime; + this.userPrefSecretDetectionRealtime = secretDetectionRealtime; + this.userPrefContainersRealtime = containersRealtime; + this.userPrefIacRealtime = iacRealtime; + this.userPreferencesSet = true; +} +``` + +### Restore Preferences (When Features Re-enable) +```java +public boolean applyUserPreferencesToRealtimeSettings() { + if (!userPreferencesSet) { + return false; // No preferences saved yet + } + + boolean changed = false; + if (ascaRealtime != userPrefAscaRealtime) { + ascaRealtime = userPrefAscaRealtime; + changed = true; + } + if (ossRealtime != userPrefOssRealtime) { + ossRealtime = userPrefOssRealtime; + changed = true; + } + // ... repeat for all scanners + return changed; +} +``` + +--- + +## Layer 3: Runtime Execution Check + +### DevAssistUtils.isScannerActive() +```java +public static boolean isScannerActive(String engineName) { + if (engineName == null) return false; + try { + if (GlobalSettingsState.getInstance().isAuthenticated()) { + ScanEngine kind = ScanEngine.valueOf(engineName.toUpperCase()); + return globalScannerController().isScannerGloballyEnabled(kind); + } + } catch (IllegalArgumentException ex) { + return false; + } + return false; +} +``` + +**Guard conditions:** +1. ✅ User is authenticated +2. ✅ Scanner is enabled in global state + +### GlobalScannerController.isScannerGloballyEnabled() +```java +public synchronized boolean isScannerGloballyEnabled(ScanEngine type) { + GlobalSettingsState state = GlobalSettingsState.getInstance(); + + // MCP disabled at tenant level → all scanners disabled + if (!state.isMcpEnabled()) { + return false; + } + + // Return scanner's individual state + return scannerStateMap.getOrDefault(type, false); +} +``` + +**Additional guards:** +1. ✅ MCP enabled at tenant level +2. ✅ Scanner enabled in individual settings + +### Where It's Used (ScanManager) +```java +protected final List> getSupportedEnabledScanner( + String filePath, PsiFile psiFile) { + List> supportedScanners = + scannerFactory.getAllSupportedScanners(filePath, psiFile); + + return supportedScanners.stream() + .filter(scannerService -> + DevAssistUtils.isScannerActive( + scannerService.getConfig().getEngineName())) + .collect(Collectors.toList()); +} +``` + +--- + +## Scenario 1: First Authentication (New User) + +### Flow: +1. User authenticates for first time +2. `userPreferencesSet = false` (no saved preferences) +3. System enables ALL scanners by default +4. All checkboxes appear checked in UI + +### Code Path (RealtimeScannersSettingsComponent): +```java +public void reset() { + state = GlobalSettingsState.getInstance(); + + // Load current state (all false initially) + ascaCheckbox.setSelected(state.isAscaRealtime()); + ossCheckbox.setSelected(state.isOssRealtime()); + secretsCheckbox.setSelected(state.isSecretDetectionRealtime()); + containersCheckbox.setSelected(state.isContainersRealtime()); + iacCheckbox.setSelected(state.isIacRealtime()); + + updateAssistState(); +} +``` + +### Question: "Why don't all scanners enable automatically?" +**Answer:** In the current implementation, they START disabled (`= false`). The JetBrains design shows that: +- If `userPreferencesSet` is false → user hasn't made choices yet +- First time showing the UI, all are disabled +- User must explicitly enable them +- Those choices are then saved as `userPreferences` + +--- + +## Scenario 2: MCP Disabled at Tenant Level (After Authentication) + +### Before Disabling MCP: +``` +Current State: ASCA=✅, OSS=✅, Secrets=✅, Containers=✅, IaC=✅ +User Preferences: Empty (not saved yet) +``` + +### When MCP Becomes Disabled: +``` +1. Check if userPreferencesSet == false +2. If true (first time MCP disabled): + - Save current state as user preferences: + userPrefAscaRealtime = true + userPrefOssRealtime = true + etc. +3. Disable all UI checkboxes and state values: + ascaRealtime = false + ossRealtime = false + etc. +4. Show error message: "MCP disabled" +``` + +### Code Implementation (RealtimeScannersSettingsComponent): +```java +private void updateUIWithMcpStatus(boolean mcpEnabled, boolean isAuthenticated) { + if (!mcpEnabled) { + // Preserve current settings before disabling + if (!state.getUserPreferencesSet()) { + state.saveCurrentSettingsAsUserPreferences(); + LOGGER.debug("[CxOneAssist] Preserved scanner settings as user preferences (MCP disabled)"); + } + + // Uncheck all checkboxes + ascaCheckbox.setSelected(false); + ossCheckbox.setSelected(false); + secretsCheckbox.setSelected(false); + containersCheckbox.setSelected(false); + iacCheckbox.setSelected(false); + + // Disable in state + state.setAscaRealtime(false); + state.setOssRealtime(false); + state.setSecretDetectionRealtime(false); + state.setContainersRealtime(false); + state.setIacRealtime(false); + + // Persist and notify + GlobalSettingsState.getInstance().apply(state); + ApplicationManager.getApplication().getMessageBus() + .syncPublisher(SettingsListener.SETTINGS_APPLIED) + .settingsApplied(); + } +} +``` + +### Scanning During MCP Disabled: +Even if a user tries to scan: +```java +DevAssistUtils.isScannerActive("ASCA") + → GlobalSettingsState.isAuthenticated() = true ✅ + → GlobalScannerController.isScannerGloballyEnabled(ASCA) + → state.isMcpEnabled() = false ❌ + → return false // Scanner doesn't run +``` + +--- + +## Scenario 3: MCP Re-enabled (Restore User Preferences) + +### Before Re-enabling MCP: +``` +Current State: ASCA=❌, OSS=❌, Secrets=❌, Containers=❌, IaC=❌ +User Preferences Set: ✅ +User Preferences: ASCA=✅, OSS=✅, Secrets=❌, Containers=✅, IaC=❌ +``` + +### When MCP Is Re-enabled: +```java +if (state.getUserPreferencesSet()) { + boolean preferencesApplied = state.applyUserPreferencesToRealtimeSettings(); + // Now state becomes: + // ASCA=✅, OSS=✅, Secrets=❌, Containers=✅, IaC=❌ + // (Exactly as user had set them before MCP was disabled) +} +``` + +### Code Path (RealtimeScannersSettingsComponent): +```java +private void updateUIWithMcpStatus(boolean mcpEnabled, boolean isAuthenticated) { + if (mcpEnabled) { + if (state.getUserPreferencesSet()) { + boolean preferencesApplied = state.applyUserPreferencesToRealtimeSettings(); + if (preferencesApplied) { + LOGGER.debug("[CxOneAssist] Restored user preferences for realtime scanners"); + // Notify listeners + ApplicationManager.getApplication().getMessageBus() + .syncPublisher(SettingsListener.SETTINGS_APPLIED) + .settingsApplied(); + } + } + + // Update UI to reflect restored preferences + ascaCheckbox.setSelected(state.isAscaRealtime()); + ossCheckbox.setSelected(state.isOssRealtime()); + secretsCheckbox.setSelected(state.isSecretDetectionRealtime()); + containersCheckbox.setSelected(state.isContainersRealtime()); + iacCheckbox.setSelected(state.isIacRealtime()); + } +} +``` + +--- + +## Scenario 4: User Manually Changes Settings + +### UI Apply Method (RealtimeScannersSettingsComponent): +```java +public void apply() { + boolean ascaSelected = ascaCheckbox.isSelected(); + boolean ossSelected = ossCheckbox.isSelected(); + boolean secretsSelected = secretsCheckbox.isSelected(); + boolean containersSelected = containersCheckbox.isSelected(); + boolean iacSelected = iacCheckbox.isSelected(); + + // Save to current state + state.setAscaRealtime(ascaSelected); + state.setOssRealtime(ossSelected); + state.setSecretDetectionRealtime(secretsSelected); + state.setContainersRealtime(containersSelected); + state.setIacRealtime(iacSelected); + + // IMPORTANT: Also save as user preferences + state.setUserPreferences(ascaSelected, ossSelected, secretsSelected, + containersSelected, iacSelected); + + // Notify all listeners + ApplicationManager.getApplication().getMessageBus() + .syncPublisher(SettingsListener.SETTINGS_APPLIED) + .settingsApplied(); +} +``` + +--- + +## Scenario 5: License Removed (Dev Assist or One Assist License) + +### Detection: +```java +private void updateAssistState() { + boolean authenticated = state.isAuthenticated(); + boolean hasAssistLicense = state.isOneAssistLicenseEnabled() || + state.isDevAssistLicenseEnabled(); + + if (!hasAssistLicense) { + // No license: hide UI and hard-disable scanners + disableAssistUI("CxOne Assist is unavailable without a license.", + JBColor.RED, + false); + return; + } + // ... continue with MCP check +} +``` + +### Disabling All Scanners (License Removed): +```java +private void disableAssistUI(String message, Color color, boolean keepVisible) { + // Preserve user preferences if not already saved + if (!state.getUserPreferencesSet()) { + state.saveCurrentSettingsAsUserPreferences(); + } + + // Uncheck and disable all UI + ascaCheckbox.setEnabled(false); + ossCheckbox.setEnabled(false); + // ... etc + ascaCheckbox.setSelected(false); + ossCheckbox.setSelected(false); + // ... etc + + // Disable in state + state.setAscaRealtime(false); + state.setOssRealtime(false); + // ... etc + + // Persist and notify + GlobalSettingsState.getInstance().apply(state); +} +``` + +--- + +## Key Design Principles + +### 1. Dual-Layer State +- **Current State**: What's active RIGHT NOW +- **User Preferences**: What user WANTS (restored when possible) + +### 2. Preservation Priority +``` +Guard Rails (in order): +1. License required? No → disable all +2. Authenticated? No → disable all +3. MCP enabled at tenant? No → disable all but preserve preferences +4. Scanner enabled individually? No → skip this scanner +``` + +### 3. Preference Persistence Logic +``` +WHEN to SAVE preferences: +- User manually changes checkboxes +- MCP about to be disabled (for the first time) +- License about to be revoked (for the first time) + +WHEN to RESTORE preferences: +- MCP becomes enabled again +- License becomes available again +- User logs in (if preferences were saved from previous session) +``` + +### 4. Execution Guard +```java +// In ScanManager: Only runs scanner if BOTH conditions true: +if (DevAssistUtils.isScannerActive(engineName)) { // Checks auth + enabled + MCP + // Scan file +} +``` + +--- + +## Implementation Checklist for Eclipse Plugin + +- [ ] Add user preference fields to Preferences.java (userPrefAsca, userPrefOss, etc.) +- [ ] Add `userPreferencesSet` flag to Preferences.java +- [ ] Implement `setUserPreferences()` method +- [ ] Implement `saveCurrentSettingsAsUserPreferences()` method +- [ ] Implement `applyUserPreferencesToRealtimeSettings()` method +- [ ] Create GlobalScannerController equivalent for Eclipse +- [ ] Create DevAssistUtils.isScannerActive() check +- [ ] Update CheckmarxPreferencePage to handle MCP enabled/disabled transitions +- [ ] Update CheckmarxPreferencePage to preserve preferences before disabling +- [ ] Update CheckmarxPreferencePage to restore preferences when re-enabling +- [ ] Ensure scanner service checks `isScannerActive()` before scanning +- [ ] Add SettingsChangeListener to notify all observers when preferences change +- [ ] Test: First auth → all disabled → user enables → MCP disabled → MCP enabled (should restore) + +--- + +## Example: Complete Auth Scenario + +``` +Timeline: +1. User installs plugin, launches Eclipse + State: authenticated=false, userPreferencesSet=false, all scanners=false + +2. User authenticates + State: authenticated=true, userPreferencesSet=false, all scanners=false + UI: All checkboxes unchecked, all disabled + +3. User opens Checkmarx Scanner Configuration settings + State: Same as above + UI: User can now enable/disable scanners + +4. User enables: ASCA ✅, OSS ✅, Secrets ❌, Containers ✅, IaC ❌ + User clicks OK/Apply + + State After Apply: + - Current: ASCA=✅, OSS=✅, Secrets=❌, Containers=✅, IaC=❌ + - Preferences: ASCA=✅, OSS=✅, Secrets=❌, Containers=✅, IaC=❌ + - userPreferencesSet=true + +5. MCP becomes disabled at tenant level + System detects: userPreferencesSet=true (already saved from step 4) + State: ASCA=❌, OSS=❌, Secrets=❌, Containers=❌, IaC=❌ (all disabled) + Preferences: Unchanged from step 4 + UI: All checkboxes unchecked and disabled, error message shown + +6. User tries to scan a file + ScanManager calls: DevAssistUtils.isScannerActive("ASCA") + → Authenticated? ✅ + → MCP enabled? ❌ + → Result: false (no scanners run) + +7. MCP becomes enabled again + System detects: userPreferencesSet=true + State: ASCA=✅, OSS=✅, Secrets=❌, Containers=✅, IaC=❌ (restored!) + UI: Checkboxes updated to reflect restored state + +8. User tries to scan a file + ScanManager calls: DevAssistUtils.isScannerActive("ASCA") + → Authenticated? ✅ + → MCP enabled? ✅ + → Scanner enabled? ✅ + → Result: true (ASCA scans) + + ScanManager calls: DevAssistUtils.isScannerActive("SECRETS") + → Authenticated? ✅ + → MCP enabled? ✅ + → Scanner enabled? ❌ + → Result: false (Secrets scanner doesn't run) +``` + +--- + +## Files to Reference + +**JetBrains Implementation:** +- GlobalSettingsState.java - State and preference methods +- RealtimeScannersSettingsComponent.java - UI and preference handling +- GlobalScannerController.java - Runtime execution check +- DevAssistUtils.isScannerActive() - Execution guard +- ScanManager.java - Uses the execution guard + +**Files to Create/Modify in Eclipse:** +- Preferences.java - Add user preference fields +- CheckmarxPreferencePage.java - Update UI handling +- GlobalScannerController.java (new) - Execution check +- DevAssistUtils.java (new) or extend existing - isScannerActive() 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/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF index 8bf051b2..2700f967 100644 --- a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF @@ -14,3 +14,4 @@ Require-Bundle: Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ClassPath: .,lib/mockito-core-5.14.2.jar,lib/powermock-core-*.jar, lib/byte-buddy-1.17.8.jar, lib/byte-buddy-agent-1.17.8.jar Automatic-Module-Name: com.checkmarx.ast.eclipse.tests +Import-Package: com.checkmarx.eclipse.common.runner 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-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java index b4028ab6..455e0dff 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java @@ -5,7 +5,7 @@ import org.mockito.Mock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.checkmarx.eclipse.runner.Authenticator; +import com.checkmarx.eclipse.common.runner.Authenticator; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/BaseIntegrationTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/BaseIntegrationTest.java index 9e0b0839..75994dbd 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/BaseIntegrationTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/BaseIntegrationTest.java @@ -12,7 +12,7 @@ import com.checkmarx.ast.wrapper.CxConfig; import com.checkmarx.ast.wrapper.CxWrapper; -import com.checkmarx.eclipse.runner.Authenticator; +import com.checkmarx.eclipse.common.runner.Authenticator; import checkmarx.ast.eclipse.plugin.tests.common.Environment; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BaseUITest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BaseUITest.java index 175febc8..346934e7 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BaseUITest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BaseUITest.java @@ -16,12 +16,13 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; + +import com.checkmarx.eclipse.common.utils.PluginConstants; + import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swt.widgets.Decorations; import org.eclipse.swt.widgets.Tree; -import com.checkmarx.eclipse.utils.PluginConstants; - import checkmarx.ast.eclipse.plugin.tests.common.Environment; public abstract class BaseUITest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BestFixLocationTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BestFixLocationTest.java index 3a4c686a..66a5e5b8 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BestFixLocationTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/BestFixLocationTest.java @@ -7,7 +7,7 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.utils.PluginConstants; public class BestFixLocationTest extends BaseUITest{ diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestFilterState.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestFilterState.java index ed66593e..4936ee24 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestFilterState.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestFilterState.java @@ -17,7 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.enums.State; import com.checkmarx.eclipse.views.actions.ToolBarActions; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestScan.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestScan.java index 64507314..2bb308f4 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestScan.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestScan.java @@ -13,7 +13,7 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarButton; import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.utils.PluginConstants; import checkmarx.ast.eclipse.plugin.tests.common.Environment; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestTriage.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestTriage.java index f2edc136..867352a4 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestTriage.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestTriage.java @@ -16,8 +16,8 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.utils.PluginConstants; public class TestTriage extends BaseUITest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestUI.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestUI.java index 7c42f499..4633bac8 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestUI.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/ui/TestUI.java @@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.views.actions.ToolBarActions; import checkmarx.ast.eclipse.plugin.tests.common.Environment; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityExtendedTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityExtendedTest.java index 6b6c4769..d6cb2551 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityExtendedTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityExtendedTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; class SeverityExtendedTest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityTest.java index cbe95be9..c27fdb4a 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/enums/SeverityTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; class SeverityTest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/properties/PreferencesTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/properties/PreferencesTest.java index 387b5c3f..4c8c6d91 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/properties/PreferencesTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/properties/PreferencesTest.java @@ -14,7 +14,7 @@ import org.mockito.MockitoAnnotations; import com.checkmarx.eclipse.Activator; -import com.checkmarx.eclipse.properties.Preferences; +import com.checkmarx.eclipse.common.preferences.Preferences; class PreferencesTest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/runner/AuthenticatorTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/runner/AuthenticatorTest.java index 3df473f4..3069ade6 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/runner/AuthenticatorTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/runner/AuthenticatorTest.java @@ -13,9 +13,9 @@ import com.checkmarx.ast.wrapper.CxException; import com.checkmarx.ast.wrapper.CxWrapper; -import com.checkmarx.eclipse.runner.Authenticator; -import com.checkmarx.eclipse.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.runner.Authenticator; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; class AuthenticatorTest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/CxLoggerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/CxLoggerTest.java index c2374a49..aa024ad6 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/CxLoggerTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/CxLoggerTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; -import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.CxLogger; class CxLoggerTest { diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java index e38bda0d..4f6ba365 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java @@ -32,9 +32,9 @@ import com.checkmarx.ast.results.result.Data; import com.checkmarx.ast.results.result.Node; import com.checkmarx.ast.results.result.Result; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.properties.Preferences; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.views.DisplayModel; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ActionOpenPreferencesPageTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ActionOpenPreferencesPageTest.java index 64d4c0fa..a54a3bd9 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ActionOpenPreferencesPageTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ActionOpenPreferencesPageTest.java @@ -15,8 +15,8 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.utils.PluginConstants; import com.checkmarx.eclipse.views.DisplayModel; import com.checkmarx.eclipse.views.actions.ActionOpenPreferencesPage; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ToolBarActionsTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ToolBarActionsTest.java index c8cdeecf..66d26119 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ToolBarActionsTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ToolBarActionsTest.java @@ -26,7 +26,7 @@ import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.enums.PluginListenerType; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.views.DisplayModel; import com.checkmarx.eclipse.views.PluginListenerDefinition; import com.checkmarx.eclipse.views.actions.ToolBarActions; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/ActionFiltersTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/ActionFiltersTest.java index 2f1ed8e5..46ce453d 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/ActionFiltersTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/ActionFiltersTest.java @@ -3,7 +3,7 @@ import com.checkmarx.eclipse.views.filters.ActionFilters; import com.checkmarx.eclipse.enums.ActionName; import com.checkmarx.eclipse.enums.PluginListenerType; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.views.PluginListenerDefinition; import com.google.common.eventbus.EventBus; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateExtendedTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateExtendedTest.java index ee9f7f79..ad4a2b8b 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateExtendedTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateExtendedTest.java @@ -8,7 +8,7 @@ import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.enums.State; import com.checkmarx.eclipse.views.GlobalSettings; import com.checkmarx.eclipse.views.filters.FilterState; diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateTest.java index ca5f312a..f1f860cf 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/filters/FilterStateTest.java @@ -12,7 +12,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.enums.State; import com.checkmarx.eclipse.views.GlobalSettings; import com.checkmarx.eclipse.views.filters.FilterState; diff --git a/checkmarx-ast-eclipse-plugin/.classpath b/checkmarx-ast-eclipse-plugin/.classpath index 32e2245e..fbfce51f 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..b52c8be5 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -5,31 +5,33 @@ 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, org.eclipse.e4.ui.di, - org.apache.commons.lang3, org.eclipse.mylyn.commons.ui, org.eclipse.mylyn.commons.core, - jakarta.inject.jakarta.inject-api;bundle-version="2.0.1" + jakarta.inject.jakarta.inject-api;bundle-version="2.0.1", + com.checkmarx.eclipse.common, + com.checkmarx.eclipse.devassist Automatic-Module-Name: com.checkmarx.eclipse Bundle-RequiredExecutionEnvironment: JavaSE-17 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.eclipse.enums, + com.checkmarx.eclipse.properties, + com.checkmarx.eclipse.utils Bundle-ClassPath: ., - lib/slf4j-simple-2.0.17.jar, - 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.4.jar, - lib/jackson-databind-2.21.5.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/org-eclipse-mylyn-commons-core.jar + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/build.properties b/checkmarx-ast-eclipse-plugin/build.properties index 680259c9..525ac1d0 100644 --- a/checkmarx-ast-eclipse-plugin/build.properties +++ b/checkmarx-ast-eclipse-plugin/build.properties @@ -2,15 +2,7 @@ output.. = bin/ bin.includes = plugin.xml,\ META-INF/,\ icons/,\ - lib/slf4j-simple-2.0.17.jar,\ - 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/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/org-eclipse-mylyn-commons-core.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/lib/ast-cli-java-wrapper-2.4.24.jar b/checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.24.jar deleted file mode 100644 index b4e1e934..00000000 --- a/checkmarx-ast-eclipse-plugin/lib/ast-cli-java-wrapper-2.4.24.jar +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e664771fd767accd5bd47057c5a6d4cc86d292c93191200d061ded6e3e527bdf -size 135732567 diff --git a/checkmarx-ast-eclipse-plugin/lib/commons-lang3-3.18.0.jar b/checkmarx-ast-eclipse-plugin/lib/commons-lang3-3.18.0.jar deleted file mode 100644 index 9359e524..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/commons-lang3-3.18.0.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/jackson-annotations-2.21.jar b/checkmarx-ast-eclipse-plugin/lib/jackson-annotations-2.21.jar deleted file mode 100644 index 8bcca189..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/jackson-annotations-2.21.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/jackson-core-2.21.4.jar b/checkmarx-ast-eclipse-plugin/lib/jackson-core-2.21.4.jar deleted file mode 100644 index e2817baf..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/jackson-core-2.21.4.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/jackson-databind-2.21.5.jar b/checkmarx-ast-eclipse-plugin/lib/jackson-databind-2.21.5.jar deleted file mode 100644 index 01f32dc7..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/jackson-databind-2.21.5.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/slf4j-api-2.0.17.jar b/checkmarx-ast-eclipse-plugin/lib/slf4j-api-2.0.17.jar deleted file mode 100644 index 26b15455..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/slf4j-api-2.0.17.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/slf4j-reload4j-2.0.17.jar b/checkmarx-ast-eclipse-plugin/lib/slf4j-reload4j-2.0.17.jar deleted file mode 100644 index 3cd24fb3..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/slf4j-reload4j-2.0.17.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/lib/slf4j-simple-2.0.17.jar b/checkmarx-ast-eclipse-plugin/lib/slf4j-simple-2.0.17.jar deleted file mode 100644 index 9a7348e8..00000000 Binary files a/checkmarx-ast-eclipse-plugin/lib/slf4j-simple-2.0.17.jar and /dev/null differ diff --git a/checkmarx-ast-eclipse-plugin/plugin.xml b/checkmarx-ast-eclipse-plugin/plugin.xml index e0f787b1..0c62d631 100644 --- a/checkmarx-ast-eclipse-plugin/plugin.xml +++ b/checkmarx-ast-eclipse-plugin/plugin.xml @@ -5,10 +5,16 @@ + + diff --git a/checkmarx-ast-eclipse-plugin/pom.xml b/checkmarx-ast-eclipse-plugin/pom.xml index cdaf8a6d..9fd07411 100644 --- a/checkmarx-ast-eclipse-plugin/pom.xml +++ b/checkmarx-ast-eclipse-plugin/pom.xml @@ -9,4 +9,7 @@ com.checkmarx.eclipse.plugin eclipse-plugin - + + src + + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/Preferences.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/Preferences.java deleted file mode 100644 index f991a1bc..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/Preferences.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.checkmarx.eclipse.properties; - -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.preferences.InstanceScope; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.ui.preferences.ScopedPreferenceStore; - -import com.checkmarx.eclipse.Activator; - -public class Preferences { - - public static final String QUALIFIER = "com.checkmarx.eclipse"; - public static final String API_KEY = "apiKey"; - public static final String ADDITIONAL_OPTIONS = "additionalOptions"; - - public static final ScopedPreferenceStore STORE = new ScopedPreferenceStore(InstanceScope.INSTANCE, QUALIFIER); - - private Preferences() { - } - - public static String getPref(String key) { - return Platform.getPreferencesService().getString(Preferences.QUALIFIER, key, null, null); - } - - public static String getApiKey() { - return getPref(API_KEY); - } - - - public static String getAdditionalOptions() { - return getPref(ADDITIONAL_OPTIONS); - } - - public static void store(String key, String value) { - IPreferenceStore prefStore = Activator.getDefault().getPreferenceStore(); - prefStore.setValue(key, value); - } - -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/PreferencesPage.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/PreferencesPage.java deleted file mode 100644 index 3c5af098..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/PreferencesPage.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.checkmarx.eclipse.properties; - -import java.util.concurrent.CompletableFuture; - -import org.eclipse.jface.preference.FieldEditor; -import org.eclipse.jface.preference.FieldEditorPreferencePage; -import org.eclipse.jface.preference.StringFieldEditor; -import org.eclipse.jface.util.PropertyChangeEvent; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -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.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Text; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPreferencePage; - -import com.checkmarx.eclipse.Activator; -import com.checkmarx.eclipse.runner.Authenticator; -import com.checkmarx.eclipse.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; -import com.checkmarx.eclipse.utils.PluginUtils; -import org.eclipse.swt.widgets.Link; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.browser.IWorkbenchBrowserSupport; -import org.eclipse.ui.PartInitException; - -import java.net.MalformedURLException; -import java.net.URL; - - -public class PreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { - public PreferencesPage() { - super(GRID); - Activator.getDefault().getPreferenceStore().addPropertyChangeListener(this::handlePropertyChange); - } - - private void handlePropertyChange(PropertyChangeEvent event) { - - } - - @Override - public void init(IWorkbench workbench) { - setPreferenceStore(Preferences.STORE); - setMessage("Checkmarx One preferences"); - } - - @Override - protected void createFieldEditors() { - Composite topComposite = new Composite(getFieldEditorParent(), SWT.NONE); - GridData topGridData = new GridData(); - topGridData.horizontalAlignment = GridData.FILL; - topGridData.verticalAlignment = GridData.FILL; - topGridData.grabExcessHorizontalSpace = true; - topComposite.setLayoutData(topGridData); - - getFieldEditorParent().setLayoutData(topGridData); - - GridLayout parentLayout = new GridLayout(); - parentLayout.numColumns = 1; - parentLayout.horizontalSpacing = 0; - parentLayout.verticalSpacing = 0; - parentLayout.marginHeight = 0; - parentLayout.marginWidth = 0; - topComposite.setLayout(parentLayout); - - StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, topComposite); - addField(apiKey); - Text textControl = apiKey.getTextControl(topComposite); - textControl.setEchoChar('*'); - - StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, - PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, StringFieldEditor.VALIDATE_ON_KEY_STROKE, topComposite); - addField(additionalParams); - - //set the width for API Key text field - GridData gridData = new GridData(SWT.BEGINNING, SWT.CENTER, true, false); - gridData.widthHint = 500; // Some width - gridData.grabExcessHorizontalSpace = false; - gridData.horizontalAlignment = GridData.FILL; - textControl.setLayoutData(gridData); - - addField(space()); - - - Link cliHelp = new Link(getFieldEditorParent(), SWT.NONE); - cliHelp.setText("CLI command that supports a set of global flags"); - cliHelp.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); - GridData linkGridData = new GridData(SWT.END, SWT.CENTER, true, false); - cliHelp.setLayoutData(linkGridData); - cliHelp.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); - try { - browserSupport.getExternalBrowser().openURL(new URL(e.text)); - } catch (PartInitException | MalformedURLException e1) { - CxLogger.error("Failed to open CLI help documentation link.", e1); - e1.printStackTrace(); - } - } - }); - - addField(space()); - - Label connectionLabel = new Label(getFieldEditorParent(), SWT.WRAP); - connectionLabel.setLayoutData( - new GridData(SWT.FILL, SWT.CENTER, true, false) - ); - - Button connectionButton = new Button(topComposite, SWT.PUSH); - connectionButton.setText(PluginConstants.PREFERENCES_TEST_CONNECTION); - connectionButton.setEnabled(!apiKey.getStringValue().trim().isEmpty()); - textControl.addModifyListener(e -> { - connectionButton.setEnabled(!textControl.getText().trim().isEmpty()); - }); - connectionButton.addSelectionListener(new SelectionAdapter() { - - public void widgetSelected(SelectionEvent e) { - - String apiKey_str = apiKey.getStringValue(); - - String additionalParams_str = additionalParams.getStringValue(); - connectionButton.setEnabled(false); - connectionLabel.setText(PluginConstants.PREFERENCES_VALIDATING_STATE); - getFieldEditorParent().layout(); - CompletableFuture.supplyAsync(() -> { - try { - return Authenticator.INSTANCE.doAuthentication( - apiKey_str, additionalParams_str); - } catch (Throwable t) { - CxLogger.error(PluginConstants.ERROR_AUTHENTICATING_AST, new Exception(t)); - return t.getMessage(); - } - }).thenAccept((result) -> Display.getDefault().syncExec(() -> { - connectionLabel.setText(mapAuthResult(result)); - getFieldEditorParent().layout(); - connectionButton.setEnabled(true); - })); - } - }); - } - - - - private static String mapAuthResult(String result) { - if (result != null && result.contains(PluginConstants.AUTH_SUCCESS_PATTERN)) { - return PluginConstants.AUTH_SUCCESS_DISPLAY; - } - return result; - } - - private FieldEditor space() { - return new LabelFieldEditor("", getFieldEditorParent()); - } - - @Override - public boolean performOk() { - boolean ok = super.performOk(); - - if (ok) { - PluginUtils.getEventBroker().post(PluginConstants.TOPIC_APPLY_SETTINGS, PluginConstants.EMPTY_STRING); - } - - return ok; - } -} \ No newline at end of file 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..90c8a271 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 @@ -6,11 +6,25 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; -import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.listener.IProjectLifecycleListener; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.devassist.backend.listener.CheckmarxEditorListener; +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.listener.ProjectLifecycleListener; public class PluginStartup implements IStartup { + static { + // Register services for PreferencesPage + Preferences.addSettingsChangeNotifier(new SettingsChangeNotifier()); + Preferences.setWorkspaceScanService(new WorkspaceScanService()); + } + 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 IProjectLifecycleListener projectListener; // Keep strong reference to prevent GC @Override public void earlyStartup() { @@ -19,13 +33,68 @@ 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); } }); } + + /** + * Get the project lifecycle listener. + * + * @return the registered ProjectLifecycleListener, or null if not yet initialized + */ + public static IProjectLifecycleListener getProjectListener() { + return projectListener; + } + + /** + * Get the real-time editor listener that tracks per-file scan jobs. + * + * @return the registered CheckmarxEditorListener, or null if not yet initialized + */ + public static CheckmarxEditorListener getRealtimeScanListener() { + return realtimeScanListener; + } + + /** + * 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/startup/SettingsChangeNotifier.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/SettingsChangeNotifier.java new file mode 100644 index 00000000..4bde6ceb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/SettingsChangeNotifier.java @@ -0,0 +1,19 @@ +package com.checkmarx.eclipse.startup; + +import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.utils.PluginUtils; + +/** + * Notifies views and components when preferences have been applied. + * + * Triggers UI updates in CheckmarxView/CxFindingsView when settings change, + * allowing them to respond to credential or configuration updates. + */ +public class SettingsChangeNotifier implements ISettingsChangeNotifier { + + @Override + public void notifySettingsApplied() { + PluginUtils.getEventBroker().post(PluginConstants.TOPIC_APPLY_SETTINGS, PluginConstants.EMPTY_STRING); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/WorkspaceScanService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/WorkspaceScanService.java new file mode 100644 index 00000000..1826b5eb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/WorkspaceScanService.java @@ -0,0 +1,38 @@ +package com.checkmarx.eclipse.startup; + +import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; +import com.checkmarx.eclipse.common.listener.IProjectLifecycleListener; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.listener.CheckmarxEditorListener; + +/** + * Triggers workspace scans after authentication. + * + * Encapsulates the ProjectLifecycleListener interaction so devassist-lib + * doesn't need to import from the main plugin. + */ +public class WorkspaceScanService implements IWorkspaceScanService { + + private static final String LOG_TAG = "[WORKSPACE-SCAN]"; + + @Override + public void scanWorkspace() { + try { + IProjectLifecycleListener projectListener = PluginStartup.getProjectListener(); + if (projectListener != null) { + CxLogger.info(LOG_TAG + " Triggering workspace OSS/IaC/container scan..."); + projectListener.rescanAllOpenProjects(); + } else { + CxLogger.warning(LOG_TAG + " Project lifecycle listener not initialized"); + } + + CheckmarxEditorListener editorListener = PluginStartup.getRealtimeScanListener(); + if (editorListener != null) { + CxLogger.info(LOG_TAG + " Triggering rescan of open editors for real-time scanners..."); + editorListener.rescanOpenEditors(); + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to trigger workspace scan: " + e.getMessage(), e); + } + } +} 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..acfdd68a 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 @@ -24,8 +24,10 @@ import com.checkmarx.ast.results.result.Node; import com.checkmarx.ast.results.result.Result; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.properties.Preferences; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.views.DisplayModel; import com.checkmarx.eclipse.views.filters.FilterState; @@ -52,7 +54,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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java index 6ccb14d1..52915d2c 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java @@ -89,13 +89,14 @@ import com.checkmarx.ast.results.result.Result; import com.checkmarx.ast.scan.Scan; import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.common.events.SettingsTopics; +import com.checkmarx.eclipse.common.preferences.Preferences; import com.checkmarx.eclipse.Activator; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.properties.Preferences; -import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.utils.NotificationPopUpUI; -import com.checkmarx.eclipse.utils.PluginConstants; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.actions.ToolBarActions; import com.checkmarx.eclipse.views.filters.FilterState; @@ -223,7 +224,7 @@ public CheckmarxView() { currentProjectId = globalSettings.getProjectId(); currentBranch = globalSettings.getBranch(); currentScanId = globalSettings.getScanId(); - PluginUtils.getEventBroker().subscribe(PluginConstants.TOPIC_APPLY_SETTINGS, this); + PluginUtils.getEventBroker().subscribe(SettingsTopics.TOPIC_APPLY_SETTINGS, this); } @Override @@ -249,9 +250,6 @@ public void dispose() { public void createPartControl(Composite parent) { this.parent = parent; - // Clear vulnerabilities from Problems View - PluginUtils.clearVulnerabilitiesFromProblemsView(); - if (PluginUtils.areCredentialsDefined()) { drawPluginPanel(); } else { @@ -333,10 +331,13 @@ public void setFocus() { * Draw Plugin */ private void drawPluginPanel() { - // Dispose missing credentials panel - if (openSettingsComposite != null && !openSettingsComposite.isDisposed()) { - openSettingsComposite.dispose(); + // Dispose all children to remove credentials panel and any other UI elements + for (Control child : parent.getChildren()) { + if (!child.isDisposed()) { + child.dispose(); + } } + openSettingsComposite = null; // Define parent layout GridLayout parentLayout = new GridLayout(); @@ -743,15 +744,18 @@ private void drawAttackVectorSeparator(Composite parent) { * Draw panel when Checkmarx credentials are not defined */ private void drawMissingCredentialsPanel() { - - // Dispose all children to remove any previous panels + // Dispose all children to remove any previous panels (plugin panel, etc.) for (Control child : parent.getChildren()) { child.dispose(); } - openSettingsComposite = new Composite(parent, SWT.NONE); + // Set parent layout for credentials panel + GridLayout parentLayout = new GridLayout(1, true); + parent.setLayout(parentLayout); + + openSettingsComposite = new Composite(parent, SWT.NONE); openSettingsComposite.setLayout(new GridLayout(1, true)); - + // This is the key line: center horizontally and vertically, and expand to fill openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); @@ -2813,33 +2817,61 @@ private void enablePluginFields(boolean enableBranchCombobox) { */ @Override public void handleEvent(org.osgi.service.event.Event arg0) { - String currentApiKey = Preferences.STORE.getString(Preferences.API_KEY); - if (!currentApiKey.isEmpty() && !isPluginDraw) { - drawPluginPanel(); - } else { - // If credentials changed reload projects, branches and scans from new tenant - if (currentApiKey.isEmpty()) { + // IEventBroker dispatches on a non-UI thread; every branch below touches SWT + // widgets, so it must be marshalled onto the display thread or it silently + // throws SWTException and the missing-credentials panel never redraws. + Display.getDefault().asyncExec(() -> { + if (parent == null || parent.isDisposed()) { + return; + } + String currentApiKey = Preferences.STORE.getString(Preferences.API_KEY); + + // Handle case: credentials just set (plugin panel not yet drawn) + if (!currentApiKey.isEmpty() && !isPluginDraw) { + CxLogger.info("Credentials detected, drawing plugin panel"); + drawPluginPanel(); + lastApiKey = currentApiKey; + return; + } + + // Handle case: credentials just removed (plugin panel is drawn) + if (currentApiKey.isEmpty() && isPluginDraw) { + CxLogger.info("Credentials removed, showing missing credentials panel"); updateStartScanButton(false); drawMissingCredentialsPanel(); - //Dispose toolbar - if (toolBarActions != null) { - toolBarActions.disposeToolbar(); - toolBarActions = null; - } + // Dispose toolbar + if (toolBarActions != null) { + toolBarActions.disposeToolbar(); + toolBarActions = null; + } isPluginDraw = false; - } else if (lastApiKey.equalsIgnoreCase(currentApiKey)) { + lastApiKey = currentApiKey; return; - } else { - // clear result section + } + + // Handle case: no credentials and panel not drawn (initial state) + if (currentApiKey.isEmpty() && !isPluginDraw) { + // Already showing missing credentials panel, nothing to do + lastApiKey = currentApiKey; + return; + } + + // Handle case: API key changed but still authenticated (plugin already drawn) + if (!currentApiKey.isEmpty() && isPluginDraw) { + if (lastApiKey != null && lastApiKey.equalsIgnoreCase(currentApiKey)) { + // Same credentials, no reload needed + return; + } + // Different credentials, reload projects/branches/scans + CxLogger.info("Credentials changed, reloading data from new tenant"); PluginUtils.clearMessage(rootModel, resultsTree); - // Reset state variables currentProjectId = PluginConstants.EMPTY_STRING; currentBranch = PluginConstants.EMPTY_STRING; currentScanId = PluginConstants.EMPTY_STRING; loadComboboxes(); + lastApiKey = currentApiKey; } - lastApiKey=currentApiKey; - } + }); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java index 9133a43e..2c7d4163 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java @@ -30,10 +30,10 @@ import com.checkmarx.ast.wrapper.CxConfig; import com.checkmarx.ast.wrapper.CxException; import com.checkmarx.ast.wrapper.CxWrapper; -import com.checkmarx.eclipse.properties.Preferences; -import com.checkmarx.eclipse.runner.Authenticator; -import com.checkmarx.eclipse.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.runner.Authenticator; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.filters.FilterState; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionCancelScan.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionCancelScan.java index 42bc5ea0..8103d7c0 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionCancelScan.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionCancelScan.java @@ -4,8 +4,8 @@ import org.eclipse.jface.viewers.TreeViewer; import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.utils.PluginConstants; import com.checkmarx.eclipse.views.DisplayModel; public class ActionCancelScan extends CxBaseAction { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionOpenPreferencesPage.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionOpenPreferencesPage.java index f1c8cc30..8f88fb92 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionOpenPreferencesPage.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionOpenPreferencesPage.java @@ -6,8 +6,8 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.dialogs.PreferencesUtil; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.enums.ActionName; -import com.checkmarx.eclipse.utils.PluginConstants; import com.checkmarx.eclipse.views.DisplayModel; public class ActionOpenPreferencesPage extends CxBaseAction { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionStartScan.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionStartScan.java index 972a5d68..d7e7aed3 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionStartScan.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ActionStartScan.java @@ -40,9 +40,9 @@ import com.checkmarx.eclipse.views.GlobalSettings; import com.checkmarx.eclipse.views.PluginListenerDefinition; import com.google.common.eventbus.EventBus; -import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.utils.NotificationPopUpUI; -import com.checkmarx.eclipse.utils.PluginConstants; import com.checkmarx.eclipse.utils.PluginUtils; public class ActionStartScan extends CxBaseAction { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ToolBarActions.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ToolBarActions.java index f2840d71..3732ec38 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ToolBarActions.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/actions/ToolBarActions.java @@ -22,9 +22,9 @@ import com.checkmarx.eclipse.enums.ActionName; import com.checkmarx.eclipse.enums.PluginListenerType; -import com.checkmarx.eclipse.enums.Severity; -import com.checkmarx.eclipse.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.views.DisplayModel; import com.checkmarx.eclipse.views.PluginListenerDefinition; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/ActionFilters.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/ActionFilters.java index 49a6ee09..bfc2fa08 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/ActionFilters.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/ActionFilters.java @@ -7,7 +7,7 @@ import com.checkmarx.eclipse.Activator; import com.checkmarx.eclipse.enums.ActionName; import com.checkmarx.eclipse.enums.PluginListenerType; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.views.DataProvider; import com.checkmarx.eclipse.views.PluginListenerDefinition; import com.google.common.eventbus.EventBus; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java index e5957112..7d435027 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java @@ -6,7 +6,7 @@ import java.util.Map; import java.util.Set; -import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.enums.State; import com.checkmarx.eclipse.views.GlobalSettings; diff --git a/com.checkmarx.eclipse.feature/feature.xml b/com.checkmarx.eclipse.feature/feature.xml index f03adb4a..faf0ab05 100644 --- a/com.checkmarx.eclipse.feature/feature.xml +++ b/com.checkmarx.eclipse.feature/feature.xml @@ -234,6 +234,20 @@ Vulnerable code is highlighted in the editor + + + + - + \ No newline at end of file diff --git a/common-lib/.classpath b/common-lib/.classpath new file mode 100644 index 00000000..31c2a6e0 --- /dev/null +++ b/common-lib/.classpath @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/common-lib/.gitignore b/common-lib/.gitignore new file mode 100644 index 00000000..92145bce --- /dev/null +++ b/common-lib/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/target/ \ No newline at end of file diff --git a/common-lib/.project b/common-lib/.project new file mode 100644 index 00000000..45eb413e --- /dev/null +++ b/common-lib/.project @@ -0,0 +1,34 @@ + + + common-lib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/common-lib/.settings/org.eclipse.core.resources.prefs b/common-lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..99f26c02 --- /dev/null +++ b/common-lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/common-lib/.settings/org.eclipse.m2e.core.prefs b/common-lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/common-lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/common-lib/META-INF/MANIFEST.MF b/common-lib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..97bd5d8d --- /dev/null +++ b/common-lib/META-INF/MANIFEST.MF @@ -0,0 +1,61 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Checkmarx Common Library +Bundle-SymbolicName: com.checkmarx.eclipse.common +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: Checkmarx +Bundle-ClassPath: ., + lib/ast-cli-java-wrapper-2.4.24.jar, + lib/jackson-core-2.21.4.jar, + lib/jackson-databind-2.21.5.jar, + lib/jackson-annotations-2.21.jar, + lib/slf4j-api-2.0.17.jar, + lib/slf4j-simple-2.0.17.jar, + lib/slf4j-reload4j-2.0.17.jar, + lib/commons-lang3-3.18.0.jar +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.ui.ide, + org.eclipse.jface.text, + org.eclipse.text, + org.eclipse.jdt.ui, + org.eclipse.ui, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Export-Package: com.checkmarx.eclipse.common.enums, + com.checkmarx.eclipse.common.events, + com.checkmarx.eclipse.common.listener, + com.checkmarx.eclipse.common.preferences, + com.checkmarx.eclipse.common.runner, + com.checkmarx.eclipse.common.utils, + org.apache.commons.lang3, + org.apache.commons.lang3.builder, + org.apache.commons.lang3.exception, + org.apache.commons.lang3.text, + org.apache.commons.lang3.time, + org.apache.commons.lang3.tuple, + com.checkmarx.ast.wrapper, + com.checkmarx.ast.project, + com.checkmarx.ast.scan, + com.checkmarx.ast.results, + com.checkmarx.ast.results.result, + com.checkmarx.ast.codebashing, + com.checkmarx.ast.learnMore, + com.checkmarx.ast.predicate, + com.checkmarx.ast.asca, + com.checkmarx.ast.containersrealtime, + com.checkmarx.ast.iacrealtime, + com.checkmarx.ast.kicsRealtimeResults, + com.checkmarx.ast.kicsRealtimeResults.ast.kicsRealtimeResult, + com.checkmarx.ast.mask, + com.checkmarx.ast.ossrealtime, + com.checkmarx.ast.realtime, + com.checkmarx.ast.remediation, + com.checkmarx.ast.secretsrealtime, + com.checkmarx.ast.tenant, + com.checkmarx.ast.utils, + com.fasterxml.jackson.annotation, + com.fasterxml.jackson.core, + com.fasterxml.jackson.core.type, + com.fasterxml.jackson.databind, + org.slf4j diff --git a/common-lib/build.properties b/common-lib/build.properties new file mode 100644 index 00000000..78e4ef65 --- /dev/null +++ b/common-lib/build.properties @@ -0,0 +1,12 @@ +output.. = bin/ +bin.includes = META-INF/,\ + lib/ast-cli-java-wrapper-2.4.24.jar,\ + lib/jackson-core-2.21.4.jar,\ + lib/jackson-databind-2.21.5.jar,\ + lib/jackson-annotations-2.21.jar,\ + lib/slf4j-api-2.0.17.jar,\ + lib/slf4j-simple-2.0.17.jar,\ + lib/slf4j-reload4j-2.0.17.jar,\ + lib/commons-lang3-3.18.0.jar,\ + . +source.. = src/ \ No newline at end of file diff --git a/common-lib/pom.xml b/common-lib/pom.xml new file mode 100644 index 00000000..c683aceb --- /dev/null +++ b/common-lib/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + com.checkmarx.ast.eclipse + checkmarx-eclipse-plugin + 1.0.0-SNAPSHOT + + com.checkmarx.eclipse.common + eclipse-plugin + diff --git a/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java b/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java new file mode 100644 index 00000000..091c187b --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java @@ -0,0 +1,17 @@ +package com.checkmarx.eclipse.common.enums; + +public enum Severity { + + CRITICAL, + HIGH, + MEDIUM, + LOW, + INFO, + GROUP_BY_SEVERITY, + GROUP_BY_QUERY_NAME, + GROUP_BY_STATE_NAME; + + public static Severity getSeverity(String severity) { + return Severity.valueOf(severity); + } +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/events/SettingsTopics.java b/common-lib/src/com/checkmarx/eclipse/common/events/SettingsTopics.java new file mode 100644 index 00000000..cac1c5d9 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/events/SettingsTopics.java @@ -0,0 +1,13 @@ +package com.checkmarx.eclipse.common.events; + +/** + * Event broker topic names shared across bundles (main plugin publishes, + * devassist and the main view subscribe). + */ +public class SettingsTopics { + + public static final String TOPIC_APPLY_SETTINGS = "ApplySettings"; + + private SettingsTopics() { + } +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IAuthenticationSuccessHandler.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IAuthenticationSuccessHandler.java new file mode 100644 index 00000000..92606af9 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IAuthenticationSuccessHandler.java @@ -0,0 +1,20 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Handler for successful authentication events. + * + * Allows devassist-lib to respond to successful authentication in PreferencesPage + * without creating a reverse dependency from common-lib to devassist-lib. + */ +public interface IAuthenticationSuccessHandler { + + /** + * Called after successful authentication and credential validation. + * + * @param mcpEnabled whether AI MCP server is enabled for the tenant + * @param logoutButton the logout button (may be disabled during flow) + * @param apiKey the newly authenticated API key + * @param additionalParams additional parameters for Checkmarx API + */ + void onAuthenticationSuccess(boolean mcpEnabled, Object logoutButton, String apiKey, String additionalParams); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IProjectLifecycleListener.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IProjectLifecycleListener.java new file mode 100644 index 00000000..5f0a82f8 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IProjectLifecycleListener.java @@ -0,0 +1,33 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Interface for handling project lifecycle and post-authentication scanning. + * + * Implemented by DevAssist module to trigger workspace scans after successful authentication. + */ +public interface IProjectLifecycleListener { + + /** + * Register this listener with Eclipse workspace. + * Must be called during plugin initialization to activate project lifecycle monitoring. + */ + void register(); + + /** + * Initiates scans for all projects already open in the workspace. + * Called after successful user authentication to ensure all open projects + * are scanned with the newly authenticated credentials. + */ + void scanAlreadyOpenProjects(); + + /** + * Re-runs the workspace file scan (manifest/IaC/container patterns) for every + * open project, regardless of whether it was already initialized. + * + * Unlike {@link #scanAlreadyOpenProjects()}, which only initializes projects + * that haven't been set up yet, this forces a fresh scan of already-initialized + * projects too. Used when scanner preferences change (e.g. a scanner is enabled) + * and previously-scanned projects need to be rescanned with the new scanner set. + */ + void rescanAllOpenProjects(); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/ISettingsChangeNotifier.java b/common-lib/src/com/checkmarx/eclipse/common/listener/ISettingsChangeNotifier.java new file mode 100644 index 00000000..5659bbe0 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/ISettingsChangeNotifier.java @@ -0,0 +1,16 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Notifies listeners when settings have been applied or changed. + * + * Allows PreferencesPage (common-lib) to notify the main plugin about settings + * changes without creating a reverse dependency. + */ +public interface ISettingsChangeNotifier { + + /** + * Notify that settings have been applied/changed. + * This triggers UI updates in views and components. + */ + void notifySettingsApplied(); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IWorkspaceScanService.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IWorkspaceScanService.java new file mode 100644 index 00000000..7aeaa6cf --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IWorkspaceScanService.java @@ -0,0 +1,17 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Service for triggering workspace scans after authentication. + * + * Allows AuthenticationSuccessHandler (devassist-lib) to trigger workspace scans + * without importing ProjectLifecycleListener or PluginStartup from main plugin. + */ +public interface IWorkspaceScanService { + + /** + * Scan all open projects in the workspace. + * Called after successful authentication to ensure all open projects + * are scanned with the newly authenticated credentials. + */ + void scanWorkspace(); +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/AuthButtonFieldEditor.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/AuthButtonFieldEditor.java similarity index 88% rename from checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/AuthButtonFieldEditor.java rename to common-lib/src/com/checkmarx/eclipse/common/preferences/AuthButtonFieldEditor.java index bd13259d..66da9dc6 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/AuthButtonFieldEditor.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/AuthButtonFieldEditor.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.properties; +package com.checkmarx.eclipse.common.preferences; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -9,9 +9,9 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; -import com.checkmarx.eclipse.runner.Authenticator; -import com.checkmarx.eclipse.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.common.runner.Authenticator; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; public class AuthButtonFieldEditor extends StringButtonFieldEditor { diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java new file mode 100644 index 00000000..3dde3b11 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java @@ -0,0 +1,300 @@ +package com.checkmarx.eclipse.common.preferences; + +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.*; +import org.eclipse.ui.dialogs.PreferencesUtil; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; + +/** + * Preference page for configuring Checkmarx scanner settings. + * Allows users to enable/disable individual scanners and select scan frequency. + */ +public class CheckmarxPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { + + // Preference Keys + public static final String PREF_ASCA_ENABLED = "scanner.asca.enabled"; + public static final String PREF_OSS_ENABLED = "scanner.oss.enabled"; + public static final String PREF_SECRETS_ENABLED = "scanner.secrets.enabled"; + public static final String PREF_CONTAINERS_ENABLED = "scanner.containers.enabled"; + public static final String PREF_IAC_ENABLED = "scanner.iac.enabled"; + public static final String PREF_CONTAINERS_TOOL = "scanner.containers.tool"; + + // Controls + private Label assistMessageLabel; + private Button ascaCheckbox; + private Button ossCheckbox; + private Button secretsCheckbox; + private Button containersCheckbox; + private Button iacCheckbox; + private Combo containersToolCombo; + private boolean loggedIn; + + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE= "Checkmarx Developer Assist Open Source Realtime Scanner (OSS-Realtime): Activate OSS-Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE="Checkmarx Developer Assist Secret Detection Realtime Scanner: Activate Secret Detection Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE= "Checkmarx Developer Assist Containers Realtime Scanner: Activate Containers Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE= "Checkmarx Developer Assist IAC Realtime Scanner: Activate IAC Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE= "Checkmarx Developer Assist AI Secure Coding Assistant (ASCA): Activate ASCA"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX= "Checkmarx Developer Assist IAC Realtime Scanner: Containers Management Tool"; + public static final String DEVASSIST_PLUGIN_WELCOME_TITLE= "Welcome to Checkmarx Developer Assist"; + public static final String CONTAINERS_TOOL_DESCRIPTION="Select the Containers Management Tool to use for IaC scanning."; + public static final String OSS_REALTIME_CHECKBOX="Scans your manifest files as you code"; + public static final String SECRETS_REALTIME_CHECKBOX="Scans your files for potential secrets and credentials as you code"; + public static final String CONTAINERS_REALTIME_CHECKBOX="Scans your Docker files and container configurations as you code"; + public static final String IAC_REALTIME_CHECKBOX="Scans your Infrastructure as Code files as you code"; + public static final String ASCA_CHECKBOX="Scan your file as you code"; + + + + public CheckmarxPreferencePage() { + super(); + setPreferenceStore(com.checkmarx.eclipse.common.preferences.Preferences.STORE); + } + + @Override + protected Control createContents(Composite parent) { + loggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + if (!loggedIn) { + return createLoggedOutContent(parent); + } + + Composite mainPanel = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.verticalSpacing = 8; + layout.horizontalSpacing = 0; + mainPanel.setLayout(layout); + mainPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // Assist Message Label (Hidden by default, red text) + assistMessageLabel = new Label(mainPanel, SWT.NONE); + assistMessageLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); + GridData msgData = new GridData(GridData.FILL_HORIZONTAL); + msgData.exclude = true; // Equivalent to hidemode 3 + assistMessageLabel.setLayoutData(msgData); + assistMessageLabel.setVisible(false); + + // --- ASCA Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); + Composite ascaComp = createIndentComposite(mainPanel); + ascaCheckbox = new Button(ascaComp, SWT.CHECK); + ascaCheckbox.setText(ASCA_CHECKBOX); + + // --- OSS Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); + Composite ossComp = createIndentComposite(mainPanel); + ossCheckbox = new Button(ossComp, SWT.CHECK); + ossCheckbox.setText(OSS_REALTIME_CHECKBOX); + + // --- Secrets Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); + Composite secretsComp = createIndentComposite(mainPanel); + secretsCheckbox = new Button(secretsComp, SWT.CHECK); + secretsCheckbox.setText(SECRETS_REALTIME_CHECKBOX); + + // --- Containers Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); + Composite containersComp = createIndentComposite(mainPanel); + containersCheckbox = new Button(containersComp, SWT.CHECK); + containersCheckbox.setText(CONTAINERS_REALTIME_CHECKBOX); + + // --- IaC Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); + Composite iacComp = createIndentComposite(mainPanel); + iacCheckbox = new Button(iacComp, SWT.CHECK); + iacCheckbox.setText(IAC_REALTIME_CHECKBOX); + + // --- Container Tool Selection Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); + Composite containerToolComp = createIndentComposite(mainPanel); + Label containerDesc = new Label(containerToolComp, SWT.WRAP); + containerDesc.setText(CONTAINERS_TOOL_DESCRIPTION); + GridData descData = new GridData(GridData.FILL_HORIZONTAL); + containerDesc.setLayoutData(descData); + + containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); + containersToolCombo.setItems(new String[] { "docker", "podman"}); + containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + loadValues(); + return mainPanel; + } + + /** + * Shown instead of the scanner checkboxes when the user isn't logged in - there + * is nothing meaningful to configure until credentials are set in "Checkmarx One". + */ + private Control createLoggedOutContent(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.marginTop = 20; + composite.setLayout(layout); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Label message = new Label(composite, SWT.WRAP); + message.setText("Log in to Checkmarx One to configure Realtime Scanners."); + message.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Link goToLoginLink = new Link(composite, SWT.NONE); + goToLoginLink.setText("Go to Checkmarx One preferences"); + goToLoginLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + goToLoginLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( + parent.getShell(), "com.checkmarx.eclipse.properties.preferencespage", null, null); + if (dialog != null) { + dialog.open(); + } + } + }); + + return composite; + } + + private Composite createIndentComposite(Composite parent) { + Composite comp = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.marginLeft = 15; + layout.marginTop = 0; + comp.setLayout(layout); + comp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + return comp; + } + + private void loadValues() { + IPreferenceStore store = getPreferenceStore(); + ascaCheckbox.setSelection(store.getBoolean(PREF_ASCA_ENABLED)); + ossCheckbox.setSelection(store.getBoolean(PREF_OSS_ENABLED)); + secretsCheckbox.setSelection(store.getBoolean(PREF_SECRETS_ENABLED)); + containersCheckbox.setSelection(store.getBoolean(PREF_CONTAINERS_ENABLED)); + iacCheckbox.setSelection(store.getBoolean(PREF_IAC_ENABLED)); + + String tool = store.getString(PREF_CONTAINERS_TOOL); + if (tool != null && !tool.isBlank()) { + containersToolCombo.setText(tool); + } else if (containersToolCombo.getItemCount() > 0) { + containersToolCombo.select(0); + } + } + + @Override + protected void performDefaults() { + if (!loggedIn) { + super.performDefaults(); + return; + } + IPreferenceStore store = getPreferenceStore(); + ascaCheckbox.setSelection(store.getDefaultBoolean(PREF_ASCA_ENABLED)); + ossCheckbox.setSelection(store.getDefaultBoolean(PREF_OSS_ENABLED)); + secretsCheckbox.setSelection(store.getDefaultBoolean(PREF_SECRETS_ENABLED)); + containersCheckbox.setSelection(store.getDefaultBoolean(PREF_CONTAINERS_ENABLED)); + iacCheckbox.setSelection(store.getDefaultBoolean(PREF_IAC_ENABLED)); + super.performDefaults(); + } + + /** + * Helper to create a titled section with a horizontal line separator. + */ + private void createSectionHeader(Composite parent, String titleText) { + Composite headerComp = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.marginWidth = 0; + layout.marginTop = 6; + layout.marginBottom = 0; + headerComp.setLayout(layout); + headerComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + int colonIndex = titleText.indexOf(":"); + + StyledText title = new StyledText(headerComp, SWT.READ_ONLY | SWT.WRAP); + title.setText(titleText); + title.setBackground(headerComp.getBackground()); // Match background color + title.setCaret(null); // Hide text cursor + + if (colonIndex != -1 && colonIndex + 1 < titleText.length()) { + int start = colonIndex + 1; // Start right after the colon + int length = titleText.length() - start; + + StyleRange boldStyle = new StyleRange(); + boldStyle.start = start; + boldStyle.length = length; + boldStyle.fontStyle = SWT.BOLD; + + title.setStyleRange(boldStyle); + + } + } + + @Override + public void init(IWorkbench workbench) { + // Initialization if needed + } + + @Override + public boolean performOk() { + if (!loggedIn) { + return super.performOk(); + } + IPreferenceStore store = getPreferenceStore(); + + // Get current UI selections + boolean ascaSelected = ascaCheckbox.getSelection(); + boolean ossSelected = ossCheckbox.getSelection(); + boolean secretsSelected = secretsCheckbox.getSelection(); + boolean containersSelected = containersCheckbox.getSelection(); + boolean iacSelected = iacCheckbox.getSelection(); + String containersTool = containersToolCombo.getText(); + + // Step 1: Save current UI state to preference store + store.setValue(PREF_ASCA_ENABLED, ascaSelected); + store.setValue(PREF_OSS_ENABLED, ossSelected); + store.setValue(PREF_SECRETS_ENABLED, secretsSelected); + store.setValue(PREF_CONTAINERS_ENABLED, containersSelected); + store.setValue(PREF_IAC_ENABLED, iacSelected); + if (containersTool != null) { + store.setValue(PREF_CONTAINERS_TOOL, containersTool); + } + + // Diagnostic: Verify what was saved + CxLogger.info("[PREFS-PAGE] Saved to preference store: ASCA=" + ascaSelected + ", OSS=" + ossSelected + + ", SECRETS=" + secretsSelected + ", CONTAINERS=" + containersSelected + ", IAC=" + iacSelected); + + // Step 2: Save as user preferences (mirrors JetBrains apply() method) + // This preserves user's choices if features toggle on/off later + Preferences.setUserPreferences(ascaSelected, ossSelected, secretsSelected, + containersSelected, iacSelected); + CxLogger.info("[PREFS-PAGE] Saved as user preferences"); + + // Step 3: Notify listeners (e.g., GlobalScannerController) about preference changes + // The listener will update GlobalScannerController based on new preferences + // This decouples CheckmarxPreferencePage from devassist-lib modules + for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { + try { + notifier.notifySettingsApplied(); + CxLogger.info("[PREFS] Notified settings change listeners"); + } catch (Exception e) { + CxLogger.warning("[PREFS] Failed to notify settings change: " + e.getMessage()); + } + } + + // Step 4: Trigger change event for listeners + store.firePropertyChangeEvent("scannerPreferencesChanged", null, null); + + return super.performOk(); + } + +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/CxPreferencesDialogSizing.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/CxPreferencesDialogSizing.java new file mode 100644 index 00000000..97d1401c --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CxPreferencesDialogSizing.java @@ -0,0 +1,48 @@ +package com.checkmarx.eclipse.common.preferences; + +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; + + +/** + * Eclipse's shared Window > Preferences dialog (WorkbenchPreferenceDialog) remembers + * its shell size across sessions. Once that remembered size is smaller than what this + * plugin's own pages need, they get clipped behind an inner scrollbar on every later + * reopen, no matter how much content they actually have. + * + * Rather than changing that shared dialog's sizing/resizing behaviour - which would + * also affect every other plugin's preference pages - this only grows the dialog + * (never shrinks it) while one of this plugin's own pages is the one actually being + * shown, right when it's first shown and again on every later switch back to it. + */ +public final class CxPreferencesDialogSizing { + + private CxPreferencesDialogSizing() { + } + + public static void applyTo(PreferenceDialog dialog) { + growIfOwnPage(dialog, dialog.getSelectedPage()); + dialog.addPageChangedListener(event -> growIfOwnPage(dialog, event.getSelectedPage())); + } + + private static void growIfOwnPage(PreferenceDialog dialog, Object page) { + if (!(page instanceof PreferencesPage) && !(page instanceof CheckmarxPreferencePage)) { + return; + } + + Shell shell = dialog.getShell(); + if (shell == null || shell.isDisposed()) { + return; + } + + Point required = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); + Point current = shell.getSize(); + int width = Math.max(required.x, current.x); + int height = Math.max(required.y, current.y); + if (width != current.x || height != current.y) { + shell.setSize(width, height); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/LabelFieldEditor.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/LabelFieldEditor.java similarity index 96% rename from checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/LabelFieldEditor.java rename to common-lib/src/com/checkmarx/eclipse/common/preferences/LabelFieldEditor.java index 7330a99d..713ce9fe 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/properties/LabelFieldEditor.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/LabelFieldEditor.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.properties; +package com.checkmarx.eclipse.common.preferences; import org.eclipse.jface.preference.FieldEditor; import org.eclipse.swt.layout.GridData; diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java new file mode 100644 index 00000000..b6c6b3e5 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java @@ -0,0 +1,189 @@ +package com.checkmarx.eclipse.common.preferences; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.ui.preferences.ScopedPreferenceStore; + +import com.checkmarx.eclipse.common.listener.IAuthenticationSuccessHandler; +import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; +import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; + +public class Preferences { + + public static final String QUALIFIER = "com.checkmarx.eclipse"; + public static final String API_KEY = "apiKey"; + public static final String ADDITIONAL_OPTIONS = "additionalOptions"; + + // Tracks whether the currently-stored API_KEY has actually been confirmed against + // the server (Authenticator.doAuthentication succeeded)... + public static final String CREDENTIALS_VALIDATED = "credentialsValidated"; + + // Scanner Preference Keys (from CheckmarxPreferencePage) + public static final String PREF_ASCA_ENABLED = "scanner.asca.enabled"; + public static final String PREF_OSS_ENABLED = "scanner.oss.enabled"; + public static final String PREF_SECRETS_ENABLED = "scanner.secrets.enabled"; + public static final String PREF_CONTAINERS_ENABLED = "scanner.containers.enabled"; + public static final String PREF_IAC_ENABLED = "scanner.iac.enabled"; + public static final String PREF_CONTAINERS_TOOL = "scanner.containers.tool"; + + // User Preferences (preserved when features toggle) - mirrors JetBrains pattern + public static final String USER_PREF_ASCA_ENABLED = "userPref.scanner.asca.enabled"; + public static final String USER_PREF_OSS_ENABLED = "userPref.scanner.oss.enabled"; + public static final String USER_PREF_SECRETS_ENABLED = "userPref.scanner.secrets.enabled"; + public static final String USER_PREF_CONTAINERS_ENABLED = "userPref.scanner.containers.enabled"; + public static final String USER_PREF_IAC_ENABLED = "userPref.scanner.iac.enabled"; + public static final String USER_PREFERENCES_SET = "userPreferences.set"; + + public static final ScopedPreferenceStore STORE = new ScopedPreferenceStore(InstanceScope.INSTANCE, QUALIFIER); + + // Handler for post-authentication UI setup (registered by devassist-lib) + private static IAuthenticationSuccessHandler authSuccessHandler; + + // Notifiers for settings changes (registered by main plugin and devassist-lib). + // A List is used because both bundles register their own notifier for different + // purposes (UI panel refresh vs. scanner-state sync); a single-slot field would + // let one registration silently overwrite the other. + private static final List settingsChangeNotifiers = new CopyOnWriteArrayList<>(); + + // Service for triggering workspace scans (registered by main plugin) + private static IWorkspaceScanService workspaceScanService; + + private Preferences() { + } + + public static String getPref(String key) { + return Platform.getPreferencesService().getString(Preferences.QUALIFIER, key, null, null); + } + + public static String getApiKey() { + return getPref(API_KEY); + } + + public static String getAdditionalOptions() { + return getPref(ADDITIONAL_OPTIONS); + } + + public static void store(String key, String value) { + // Replaced Activator call with the ScopedPreferenceStore instance + STORE.setValue(key, value); + } + + public static void clearApiKey() { + STORE.setValue(API_KEY, ""); + STORE.setValue(CREDENTIALS_VALIDATED, false); + } + + public static boolean isCredentialsValidated() { + return STORE.getBoolean(CREDENTIALS_VALIDATED); + } + + public static void setCredentialsValidated(boolean validated) { + STORE.setValue(CREDENTIALS_VALIDATED, validated); + } + + public static void setAuthenticationSuccessHandler(IAuthenticationSuccessHandler handler) { + authSuccessHandler = handler; + } + + public static IAuthenticationSuccessHandler getAuthenticationSuccessHandler() { + return authSuccessHandler; + } + + public static void addSettingsChangeNotifier(ISettingsChangeNotifier notifier) { + settingsChangeNotifiers.add(notifier); + } + + public static List getSettingsChangeNotifiers() { + return settingsChangeNotifiers; + } + + public static void setWorkspaceScanService(IWorkspaceScanService service) { + workspaceScanService = service; + } + + public static IWorkspaceScanService getWorkspaceScanService() { + return workspaceScanService; + } + + // ============================================================================ + // USER PREFERENCES - Preserve user's scanner choices across feature toggles + // Mirrors JetBrains GlobalSettingsState.setUserPreferences() pattern + // ============================================================================ + + /** + * Save user's current scanner preferences for preservation when features toggle. + * Called when user clicks OK/Apply on preferences page, or when a feature is about to disable. + * + * @param asca Enable/disable ASCA + * @param oss Enable/disable OSS + * @param secrets Enable/disable Secrets + * @param containers Enable/disable Containers + * @param iac Enable/disable IaC + */ + public static void setUserPreferences(boolean asca, boolean oss, boolean secrets, + boolean containers, boolean iac) { + STORE.setValue(USER_PREF_ASCA_ENABLED, asca); + STORE.setValue(USER_PREF_OSS_ENABLED, oss); + STORE.setValue(USER_PREF_SECRETS_ENABLED, secrets); + STORE.setValue(USER_PREF_CONTAINERS_ENABLED, containers); + STORE.setValue(USER_PREF_IAC_ENABLED, iac); + STORE.setValue(USER_PREFERENCES_SET, true); + } + + /** + * Restore user's previously saved preferences to current scanner settings. + * Called when a feature re-enables after being disabled. + * + * @return true if preferences were restored, false if no preferences saved + */ + public static boolean applyUserPreferencesToCurrentSettings() { + if (!STORE.getBoolean(USER_PREFERENCES_SET)) { + return false; // No user preferences saved yet + } + + boolean asca = STORE.getBoolean(USER_PREF_ASCA_ENABLED); + boolean oss = STORE.getBoolean(USER_PREF_OSS_ENABLED); + boolean secrets = STORE.getBoolean(USER_PREF_SECRETS_ENABLED); + boolean containers = STORE.getBoolean(USER_PREF_CONTAINERS_ENABLED); + boolean iac = STORE.getBoolean(USER_PREF_IAC_ENABLED); + + // Apply to current settings + STORE.setValue(PREF_ASCA_ENABLED, asca); + STORE.setValue(PREF_OSS_ENABLED, oss); + STORE.setValue(PREF_SECRETS_ENABLED, secrets); + STORE.setValue(PREF_CONTAINERS_ENABLED, containers); + STORE.setValue(PREF_IAC_ENABLED, iac); + + return true; + } + + /** + * Check if user has any custom preferences saved. + * Used to determine if this is first time or existing user. + * + * @return true if preferences have been saved, false if default state + */ + public static boolean getUserPreferencesSet() { + return STORE.getBoolean(USER_PREFERENCES_SET); + } + + /** + * Save current scanner settings as user preferences. + * Called before disabling scanners to preserve user's choices. + */ + public static void saveCurrentSettingsAsUserPreferences() { + boolean asca = STORE.getBoolean(PREF_ASCA_ENABLED); + boolean oss = STORE.getBoolean(PREF_OSS_ENABLED); + boolean secrets = STORE.getBoolean(PREF_SECRETS_ENABLED); + boolean containers = STORE.getBoolean(PREF_CONTAINERS_ENABLED); + boolean iac = STORE.getBoolean(PREF_IAC_ENABLED); + + setUserPreferences(asca, oss, secrets, containers, iac); + } +} \ No newline at end of file diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java new file mode 100644 index 00000000..a56832a5 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -0,0 +1,368 @@ +package com.checkmarx.eclipse.common.preferences; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.preference.FieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWorkbenchBrowserSupport; +import org.eclipse.ui.dialogs.PreferencesUtil; + +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.listener.IAuthenticationSuccessHandler; +import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; +import com.checkmarx.eclipse.common.runner.Authenticator; +import com.checkmarx.eclipse.common.runner.TenantSettingsProvider; +import com.checkmarx.eclipse.common.utils.CxLogger; + +public class PreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + // Captured once the fields are loaded, so performOk() can tell whether THIS + // page's own settings actually changed. Needed because Eclipse's shared + // Preferences dialog calls performOk() on every page the user visited during + // the session - not just the one they edited - so simply opening/looking at + // "Checkmarx One" while really only changing "Checkmarx Scanner Configuration" + // (Realtime Scanners) would otherwise still unconditionally fire + // TOPIC_APPLY_SETTINGS below and refresh the unrelated Checkmarx One scan view. + private StringFieldEditor apiKeyField; + private StringFieldEditor additionalParamsField; + private String initialApiKey; + private String initialAdditionalOptions; + private Link realtimeScannersLink; + + public PreferencesPage() { + super(GRID); + // Replaced Activator preference store listener with Preferences.STORE + Preferences.STORE.addPropertyChangeListener(this::handlePropertyChange); + } + + private void handlePropertyChange(PropertyChangeEvent event) { + refreshRealtimeScannersLink(); + } + + /** + * Shows the "Go to Realtime Scanners" link only while the user is logged in - + * the page it opens has no meaningful content to configure otherwise. + */ + private void refreshRealtimeScannersLink() { + if (realtimeScannersLink != null && !realtimeScannersLink.isDisposed()) { + boolean isLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + + realtimeScannersLink.setVisible(isLoggedIn); + + if (realtimeScannersLink.getLayoutData() instanceof GridData) { + ((GridData) realtimeScannersLink.getLayoutData()).exclude = !isLoggedIn; + } + + // Re-layout the parent so other controls adjust dynamically + Composite parent = realtimeScannersLink.getParent(); + if (parent != null && !parent.isDisposed()) { + parent.layout(true, true); + } + } + } + + @Override + public void init(IWorkbench workbench) { + setPreferenceStore(Preferences.STORE); + setMessage("Checkmarx One preferences"); + } + + @Override + protected void createFieldEditors() { + Composite topComposite = new Composite(getFieldEditorParent(), SWT.NONE); + GridData topGridData = new GridData(); + topGridData.horizontalAlignment = GridData.FILL; + topGridData.verticalAlignment = GridData.FILL; + topGridData.grabExcessHorizontalSpace = true; + topComposite.setLayoutData(topGridData); + + getFieldEditorParent().setLayoutData(topGridData); + + GridLayout parentLayout = new GridLayout(); + parentLayout.numColumns = 1; + parentLayout.horizontalSpacing = 0; + parentLayout.verticalSpacing = 0; + parentLayout.marginHeight = 0; + parentLayout.marginWidth = 0; + topComposite.setLayout(parentLayout); + + StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, topComposite); + apiKeyField = apiKey; + addField(apiKey); + Text textControl = apiKey.getTextControl(topComposite); + textControl.setEchoChar('*'); + + StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, + PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, StringFieldEditor.VALIDATE_ON_KEY_STROKE, topComposite); + additionalParamsField = additionalParams; + addField(additionalParams); + + // Baseline for the change-detection guard in performOk() - captured now that + // both fields have loaded their values from the preference store. + initialApiKey = apiKey.getStringValue(); + initialAdditionalOptions = additionalParams.getStringValue(); + + //set the width for API Key text field + GridData gridData = new GridData(SWT.BEGINNING, SWT.CENTER, true, false); + gridData.widthHint = 500; // Some width + gridData.grabExcessHorizontalSpace = false; + gridData.horizontalAlignment = GridData.FILL; + textControl.setLayoutData(gridData); + + addField(space()); + + + Link cliHelp = new Link(getFieldEditorParent(), SWT.NONE); + cliHelp.setText("CLI command that supports a set of global flags"); + cliHelp.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + GridData linkGridData = new GridData(SWT.END, SWT.CENTER, true, false); + cliHelp.setLayoutData(linkGridData); + cliHelp.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); + try { + browserSupport.getExternalBrowser().openURL(new URL(e.text)); + } catch (PartInitException | MalformedURLException e1) { + CxLogger.error("Failed to open CLI help documentation link.", e1); + e1.printStackTrace(); + } + } + }); + + addField(space()); + + Label connectionLabel = new Label(getFieldEditorParent(), SWT.WRAP); + connectionLabel.setLayoutData( + new GridData(SWT.FILL, SWT.CENTER, true, false) + ); + + // Holds the Logout button reference so the Connect handler (defined before the + // Logout button is created below) can disable/enable it during the connect flow. + final Button[] logoutButtonHolder = new Button[1]; + + Button connectionButton = new Button(topComposite, SWT.PUSH); + connectionButton.setText(PluginConstants.PREFERENCES_TEST_CONNECTION); + connectionButton.setEnabled(!apiKey.getStringValue().trim().isEmpty()); + textControl.addModifyListener(e -> { + connectionButton.setEnabled(!textControl.getText().trim().isEmpty()); + + // Any edit means whatever gets saved next (even via Apply/OK without ever + // clicking Test Connection) hasn't been checked against the server, so it must + // not keep looking "connected" on the strength of a previous, different key's + // validation. + Preferences.setCredentialsValidated(false); + }); + connectionButton.addSelectionListener(new SelectionAdapter() { + + public void widgetSelected(SelectionEvent e) { + + String apiKey_str = apiKey.getStringValue(); + + String additionalParams_str = additionalParams.getStringValue(); + connectionButton.setEnabled(false); + connectionLabel.setText(PluginConstants.PREFERENCES_VALIDATING_STATE); + getFieldEditorParent().layout(); + + // Disable Logout for the duration of the connect/validate flow so a user can't + // interrupt it mid-flight (e.g. closing the dialog or logging out) in a way that + // leaves the flow half-finished and the welcome dialog never shown. + if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { + logoutButtonHolder[0].setEnabled(false); + } + + CompletableFuture.supplyAsync(() -> { + try { + return Authenticator.INSTANCE.doAuthentication( + apiKey_str, additionalParams_str); + } catch (Throwable t) { + CxLogger.error(PluginConstants.ERROR_AUTHENTICATING_AST, new Exception(t)); + return t.getMessage(); + } + }).thenAccept((result) -> Display.getDefault().syncExec(() -> { + // Guard every widget touch below: if the preferences dialog was closed + // while this connect/validate call was in flight, these are disposed. + // Previously an unguarded call here threw and aborted this whole runnable, + // which is why the welcome dialog never appeared after closing the dialog. + if (!connectionButton.isDisposed()) { + connectionButton.setEnabled(true); + } + + // Show welcome dialog on successful authentication. The "Validating..." + // message is left on screen (not switched to "Connected") until the + // welcome dialog is actually about to appear, so the label never claims + // success before the user sees the welcome page. + if (result != null && result.contains(PluginConstants.AUTH_SUCCESS_PATTERN)) { + // The key was only just validated by "Test Connection" - it isn't persisted + // to the store until the user clicks OK/Apply on this dialog, which they may + // never do once they see the Welcome page. Persist it now so + // isUserAuthenticated() (checked by ProjectLifecycleListener, and by + // anything else gated on login) actually sees it. + Preferences.STORE.setValue(Preferences.API_KEY, apiKey_str); + Preferences.STORE.setValue(Preferences.ADDITIONAL_OPTIONS, additionalParams_str); + Preferences.setCredentialsValidated(true); + refreshRealtimeScannersLink(); + + // Notify views (CheckmarxView/CxFindingsView) that credentials are now available + // so they can switch from the credentials panel to the actual work views + for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { + notifier.notifySettingsApplied(); + } + + // Fetch MCP enabled status from server asynchronously + CompletableFuture.supplyAsync(() -> { + try { + return TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled( + apiKey_str, additionalParams_str); + } catch (Exception ex) { + CxLogger.error("Failed to fetch MCP status", ex); + return false; + } + }).thenAccept((mcpEnabled) -> Display.getDefault().syncExec(() -> { + if (!connectionLabel.isDisposed()) { + connectionLabel.setText(mapAuthResult(result)); + } + if (!getFieldEditorParent().isDisposed()) { + getFieldEditorParent().layout(); + } + // Delegate to handler registered by devassist-lib (if available) + IAuthenticationSuccessHandler handler = Preferences.getAuthenticationSuccessHandler(); + if (handler != null) { + handler.onAuthenticationSuccess(mcpEnabled, logoutButtonHolder[0], apiKey_str, additionalParams_str); + } else { + CxLogger.warning("[PREFS] No authentication success handler registered - welcome dialog skipped"); + if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { + logoutButtonHolder[0].setEnabled(true); + } + } + })); + } else { + // Authentication failed - the flow ends here with no welcome dialog, + // so show the failure message right away and restore Logout. + if (!connectionLabel.isDisposed()) { + connectionLabel.setText(mapAuthResult(result)); + } + if (!getFieldEditorParent().isDisposed()) { + getFieldEditorParent().layout(); + } + if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { + logoutButtonHolder[0].setEnabled(true); + } + } + })); + } + }); + + addField(space()); + + Button logoutButton = new Button(topComposite, SWT.PUSH); + logoutButtonHolder[0] = logoutButton; + logoutButton.setText("Logout"); + logoutButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Preferences.clearApiKey(); + apiKey.setStringValue(""); +// textControl.setText(""); +// connectionLabel.setText(""); + refreshRealtimeScannersLink(); + getFieldEditorParent().layout(); + + // Redraws the missing-credentials panel in CheckmarxView/CxFindingsView right + // away. Without this, they only learn credentials are gone once performOk() + // runs (i.e. the user clicks OK/Apply) - if they instead Cancel or just close + // the dialog after Logout, both views kept showing stale "connected" content. + // Notify main plugin that settings have changed + for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { + notifier.notifySettingsApplied(); + } + } + }); + + addField(space()); + + realtimeScannersLink = new Link(getFieldEditorParent(), SWT.NONE); + realtimeScannersLink.setText("Go to Realtime Scanners"); + realtimeScannersLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + + // Call refresh after setting the LayoutData + refreshRealtimeScannersLink(); + + realtimeScannersLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( + getShell(), + "com.checkmarx.eclipse.devassist.prefs.checkmarxpreferencepage", + null, + null + ); + if (dialog != null) { + CxPreferencesDialogSizing.applyTo(dialog); + dialog.open(); + } + } + }); + } + + private static String mapAuthResult(String result) { + if (result != null && result.contains(PluginConstants.AUTH_SUCCESS_PATTERN)) { + return PluginConstants.AUTH_SUCCESS_DISPLAY; + } + return result; + } + + private FieldEditor space() { + return new LabelFieldEditor("", getFieldEditorParent()); + } + + @Override + public boolean performOk() { + boolean ok = super.performOk(); + + if (ok) { + // Only notify listeners (e.g. the Checkmarx One scan view refresh) if this + // page's own settings actually changed in this session. Without this guard, + // merely having visited this page in the same Preferences dialog session as + // the unrelated "Checkmarx Scanner Configuration" (Realtime Scanners) page - + // a sibling top-level page in the same tree - is enough for Eclipse to call + // this performOk() too when the user only meant to save realtime scanner + // settings, spuriously refreshing the Checkmarx One scan window. + String currentApiKey = apiKeyField != null ? apiKeyField.getStringValue() : null; + String currentAdditionalOptions = additionalParamsField != null ? additionalParamsField.getStringValue() : null; + boolean settingsActuallyChanged = + !java.util.Objects.equals(currentApiKey, initialApiKey) + || !java.util.Objects.equals(currentAdditionalOptions, initialAdditionalOptions); + + if (settingsActuallyChanged) { + // Notify main plugin that settings have changed + for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { + notifier.notifySettingsApplied(); + } + } + } + + return ok; + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/runner/Authenticator.java b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java similarity index 89% rename from checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/runner/Authenticator.java rename to common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java index 202baa99..61c497ce 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/runner/Authenticator.java +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.runner; +package com.checkmarx.eclipse.common.runner; import java.io.IOException; import org.slf4j.Logger; @@ -7,8 +7,8 @@ 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.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; public class Authenticator { private final Logger log; diff --git a/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java b/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java new file mode 100644 index 00000000..f4ed02e8 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java @@ -0,0 +1,52 @@ +package com.checkmarx.eclipse.common.runner; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Provides tenant-specific settings from the Checkmarx API. + * Fetches configuration details like MCP enablement status. + */ +public class TenantSettingsProvider { + private static final Logger log = LoggerFactory.getLogger(TenantSettingsProvider.class); + public static final TenantSettingsProvider INSTANCE = new TenantSettingsProvider(); + + private TenantSettingsProvider() { + } + + /** + * Check if AI MCP (Checkmarx One Assist) is enabled for the current tenant + * + * @param apiKey API key for authentication + * @param additionalParams Additional parameters for the CxWrapper + * @return true if MCP is enabled, false otherwise + */ + public boolean isAiMcpServerEnabled(String apiKey, String additionalParams) { + if (apiKey == null || apiKey.trim().isEmpty()) { + return false; + } + + try { + CxConfig config = CxConfig.builder() + .apiKey(apiKey) + .additionalParameters(additionalParams) + .build(); + + CxWrapper wrapper = new CxWrapper(config, log); + boolean mcpEnabled = wrapper.aiMcpServerEnabled(); + CxLogger.info(String.format("MCP Server Status: %s", mcpEnabled ? "ENABLED" : "DISABLED")); + return mcpEnabled; + } catch (IOException | InterruptedException | CxException e) { + CxLogger.error("Failed to check MCP server status: " + e.getMessage(), e); + // Default to false on error to be conservative + return false; + } + } +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/CxLogger.java b/common-lib/src/com/checkmarx/eclipse/common/utils/CxLogger.java new file mode 100644 index 00000000..099f8e14 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/CxLogger.java @@ -0,0 +1,58 @@ +package com.checkmarx.eclipse.common.utils; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +/** + * Class responsible to add entries to Eclipse Error Log perspective + * + * @author HugoMa + * + */ +public class CxLogger { + + private static final Bundle BUNDLE = FrameworkUtil.getBundle(CxLogger.class); + private static final ILog LOGGER = Platform.getLog(BUNDLE); + + /** + * Add entry as error + * + * @param msg + * @param e + */ + public static void error(String msg, Exception e) { + log(Status.ERROR, msg, e); + } + + /** + * Add entry as warning + * + * @param msg + */ + public static void warning(String msg) { + log(Status.WARNING, msg, null); + } + + /** + * Add entry as info + * + * @param msg + */ + public static void info(String msg) { + log(Status.INFO, msg, null); + } + + /** + * Add entry to Error Log + * + * @param status + * @param msg + * @param e + */ + private static void log(int status, String msg, Exception e) { + LOGGER.log(new Status(status, BUNDLE.getSymbolicName(), msg, e)); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java similarity index 96% rename from checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java rename to common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index 524b1136..76272e05 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -1,4 +1,6 @@ -package com.checkmarx.eclipse.utils; +package com.checkmarx.eclipse.common.utils; + +import com.checkmarx.eclipse.common.events.SettingsTopics; public class PluginConstants { public static final String EMPTY_STRING = ""; @@ -21,6 +23,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 ********************************/ @@ -62,9 +65,7 @@ public class PluginConstants { public static final String PREFERENCES_ADDITIONAL_OPTIONS = "Additional Params:"; public static final String PREFERENCES_TEST_CONNECTION = "Test Connection"; public static final String PREFERENCES_VALIDATING_STATE = "Validating..."; - - /******************************** TOPICS ********************************/ - public static final String TOPIC_APPLY_SETTINGS = "ApplySettings"; + public static final String TOPIC_APPLY_SETTINGS = SettingsTopics.TOPIC_APPLY_SETTINGS; /******************************** PROBLEMS VIEW ********************************/ public static final String PROBLEM_SOURCE_ID = "CheckmarxEclipsePlugin"; diff --git a/devassist-lib/.classpath b/devassist-lib/.classpath new file mode 100644 index 00000000..43dc9a78 --- /dev/null +++ b/devassist-lib/.classpath @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devassist-lib/.gitignore b/devassist-lib/.gitignore new file mode 100644 index 00000000..92145bce --- /dev/null +++ b/devassist-lib/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/target/ \ No newline at end of file diff --git a/devassist-lib/.project b/devassist-lib/.project new file mode 100644 index 00000000..7c973ce5 --- /dev/null +++ b/devassist-lib/.project @@ -0,0 +1,34 @@ + + + devassist-lib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/devassist-lib/.settings/org.eclipse.core.resources.prefs b/devassist-lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..99f26c02 --- /dev/null +++ b/devassist-lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/devassist-lib/.settings/org.eclipse.m2e.core.prefs b/devassist-lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/devassist-lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/devassist-lib/META-INF/MANIFEST.MF b/devassist-lib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..71272e26 --- /dev/null +++ b/devassist-lib/META-INF/MANIFEST.MF @@ -0,0 +1,28 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: DevAssist Library +Bundle-SymbolicName: com.checkmarx.eclipse.devassist;singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Activator: com.checkmarx.eclipse.devassist.Activator +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Bundle-ClassPath: . +Require-Bundle: com.checkmarx.eclipse.common, + org.eclipse.ui, + org.eclipse.ui.workbench, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors, + org.eclipse.ui.ide, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.core.commands, + org.eclipse.jface, + org.eclipse.jface.text, + org.eclipse.swt, + org.eclipse.jgit, + org.eclipse.e4.core.services, + org.eclipse.e4.ui.css.swt.theme +Import-Package: org.eclipse.mylyn.commons.ui.dialogs, + org.osgi.service.event;version="1.4.1" +Export-Package: com.checkmarx.eclipse.devassist.backend, + com.checkmarx.eclipse.devassist.backend.listener diff --git a/devassist-lib/build.properties b/devassist-lib/build.properties new file mode 100644 index 00000000..f62b840c --- /dev/null +++ b/devassist-lib/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + plugin.xml,\ + icons/,\ + . \ No newline at end of file diff --git a/devassist-lib/icons/CxFlatLogo16x16.png b/devassist-lib/icons/CxFlatLogo16x16.png new file mode 100644 index 00000000..4176de23 Binary files /dev/null and b/devassist-lib/icons/CxFlatLogo16x16.png differ diff --git a/devassist-lib/icons/checkmarx-plugin-13_dark.png b/devassist-lib/icons/checkmarx-plugin-13_dark.png new file mode 100644 index 00000000..18aa6461 Binary files /dev/null and b/devassist-lib/icons/checkmarx-plugin-13_dark.png differ diff --git a/devassist-lib/icons/critical_16.svg b/devassist-lib/icons/critical_16.svg new file mode 100644 index 00000000..6e1929e8 --- /dev/null +++ b/devassist-lib/icons/critical_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/critical_16_dark.svg b/devassist-lib/icons/critical_16_dark.svg new file mode 100644 index 00000000..9c89888d --- /dev/null +++ b/devassist-lib/icons/critical_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/high_16.svg b/devassist-lib/icons/high_16.svg new file mode 100644 index 00000000..4c815e84 --- /dev/null +++ b/devassist-lib/icons/high_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/high_16_dark.svg b/devassist-lib/icons/high_16_dark.svg new file mode 100644 index 00000000..d9b8a81f --- /dev/null +++ b/devassist-lib/icons/high_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_16.svg b/devassist-lib/icons/ignored_16.svg new file mode 100644 index 00000000..4ec04da0 --- /dev/null +++ b/devassist-lib/icons/ignored_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_16_dark.svg b/devassist-lib/icons/ignored_16_dark.svg new file mode 100644 index 00000000..20246d56 --- /dev/null +++ b/devassist-lib/icons/ignored_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/low_16.svg b/devassist-lib/icons/low_16.svg new file mode 100644 index 00000000..40b203e4 --- /dev/null +++ b/devassist-lib/icons/low_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/low_16_dark.svg b/devassist-lib/icons/low_16_dark.svg new file mode 100644 index 00000000..69f9b3a6 --- /dev/null +++ b/devassist-lib/icons/low_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/malicious_16.svg b/devassist-lib/icons/malicious_16.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/malicious_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/malicious_16_dark.svg b/devassist-lib/icons/malicious_16_dark.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/malicious_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/medium_16.svg b/devassist-lib/icons/medium_16.svg new file mode 100644 index 00000000..3a6cda49 --- /dev/null +++ b/devassist-lib/icons/medium_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/medium_16_dark.svg b/devassist-lib/icons/medium_16_dark.svg new file mode 100644 index 00000000..5be2c823 --- /dev/null +++ b/devassist-lib/icons/medium_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ok_16.svg b/devassist-lib/icons/ok_16.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/ok_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ok_16_dark.svg b/devassist-lib/icons/ok_16_dark.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/ok_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/unknown_16.svg b/devassist-lib/icons/unknown_16.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/devassist-lib/icons/unknown_16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/unknown_16_dark.svg b/devassist-lib/icons/unknown_16_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/devassist-lib/icons/unknown_16_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/welcomePageScanner.svg b/devassist-lib/icons/welcomePageScanner.svg new file mode 100644 index 00000000..e84b61ee --- /dev/null +++ b/devassist-lib/icons/welcomePageScanner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devassist-lib/icons/welcomePageScanner_dark.svg b/devassist-lib/icons/welcomePageScanner_dark.svg new file mode 100644 index 00000000..798ceb92 --- /dev/null +++ b/devassist-lib/icons/welcomePageScanner_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devassist-lib/plugin.xml b/devassist-lib/plugin.xml new file mode 100644 index 00000000..0cf8e3f3 --- /dev/null +++ b/devassist-lib/plugin.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devassist-lib/pom.xml b/devassist-lib/pom.xml new file mode 100644 index 00000000..42f73309 --- /dev/null +++ b/devassist-lib/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + com.checkmarx.ast.eclipse + checkmarx-eclipse-plugin + 1.0.0-SNAPSHOT + + com.checkmarx.eclipse.devassist + eclipse-plugin + diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/Activator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/Activator.java new file mode 100644 index 00000000..c52616db --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/Activator.java @@ -0,0 +1,84 @@ +package com.checkmarx.eclipse.devassist; + +import org.eclipse.core.runtime.Plugin; +import org.osgi.framework.BundleContext; + +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.ScannerPreferencesListener; +import com.checkmarx.eclipse.devassist.configuration.McpInstallService; + +/** + * Devassist library activator. + * Initializes McpInstallService to register authentication handlers. + */ +public class Activator extends Plugin { + + public static final String PLUGIN_ID = "com.checkmarx.eclipse.devassist"; + + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + try { + // Step 1: Register scanner preferences listener + // Bridges CheckmarxPreferencePage changes to GlobalScannerController + ScannerPreferencesListener preferencesListener = new ScannerPreferencesListener(); + Preferences.addSettingsChangeNotifier(preferencesListener); + CxLogger.info("[DEVASSIST] Registered ScannerPreferencesListener"); + + // Step 2: Initialize GlobalScannerController with current preferences + // Ensures scanner execution guards use latest stored preferences + GlobalScannerController controller = GlobalScannerController.getInstance(); + + // Load preferences from store and sync with controller + boolean ascaEnabled = Preferences.STORE.getBoolean(Preferences.PREF_ASCA_ENABLED); + boolean ossEnabled = Preferences.STORE.getBoolean(Preferences.PREF_OSS_ENABLED); + boolean secretsEnabled = Preferences.STORE.getBoolean(Preferences.PREF_SECRETS_ENABLED); + boolean containersEnabled = Preferences.STORE.getBoolean(Preferences.PREF_CONTAINERS_ENABLED); + boolean iacEnabled = Preferences.STORE.getBoolean(Preferences.PREF_IAC_ENABLED); + + CxLogger.info("[ACTIVATOR] Initial preferences loaded: ASCA=" + ascaEnabled + ", OSS=" + ossEnabled + + ", SECRETS=" + secretsEnabled + ", CONTAINERS=" + containersEnabled + ", IAC=" + iacEnabled); + + // Sync preferences to controller (mirrors JetBrains initialization) + if (ascaEnabled) controller.enableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.ASCA); + else controller.disableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.ASCA); + + if (ossEnabled) controller.enableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.OSS); + else controller.disableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.OSS); + + if (secretsEnabled) controller.enableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.SECRETS); + else controller.disableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.SECRETS); + + if (containersEnabled) controller.enableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.CONTAINERS); + else controller.disableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.CONTAINERS); + + if (iacEnabled) controller.enableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.IAC); + else controller.disableScanner(com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType.IAC); + + CxLogger.info("[DEVASSIST] Initialized GlobalScannerController with preferences. " + + controller.getStateReport()); + + } catch (Exception e) { + CxLogger.error("[DEVASSIST] Error during initialization: " + e.getMessage(), e); + } + + try { + // Step 3: Register authentication handlers (existing code) + // Calling a real static member (not just the .class literal) is what forces the JVM + // to run McpInstallService's static initializer, which registers the auth handlers. + // This also does its documented job: auto-install MCP if already authenticated. + McpInstallService.attemptAutoInstall(); + CxLogger.info("[DEVASSIST] Initialized authentication handlers"); + } catch (Exception e) { + CxLogger.error("[DEVASSIST] Error registering authentication handlers: " + e.getMessage(), e); + } + } + + @Override + public void stop(BundleContext context) throws Exception { + super.stop(context); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/Constants.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/Constants.java new file mode 100644 index 00000000..a57aefe9 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/Constants.java @@ -0,0 +1,31 @@ +package com.checkmarx.eclipse.devassist.backend; + +/** + * Constants for DevAssist backend operations. + * Mirrors JetBrains Constants pattern. + */ +public class Constants { + // Main plugin bundle id, used to load icons/resources that live in the main plugin bundle + public static final String MAIN_PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + + // UI strings + public static final String BTN_OPEN_SETTINGS = "Open Settings"; + public static final String FINDINGS_PROMO_DESCRIPTION = "Checkmarx Developer Assist stops vulnerabilities where your code is written, with fixes you can actually trust."; + + // Log messages + public static final String ERROR_BUILDING_CX_WRAPPER = "An error occurred while instantiating a CxWrapper: %s"; + + // 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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java new file mode 100644 index 00000000..503d066f --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java new file mode 100644 index 00000000..2d4e0a15 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScanStateCacheClearer.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScanStateCacheClearer.java new file mode 100644 index 00000000..1880325d --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScanStateCacheClearer.java @@ -0,0 +1,66 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; + +/** + * Clears file state cache for scanners that are being re-enabled. + * + * When a workspace scanner (OSS, IaC, Container) is disabled, findings are purged. + * When re-enabled, the state cache (in DevAssistScanStateHolder) still holds old + * file hashes, preventing fresh scans. This clears the cache for those scanners + * so manifest files get re-scanned immediately. + * + * Mirrors ScannerMarkerPurger pattern for disabled scanners. + */ +public class ScanStateCacheClearer { + + private static final String LOG_TAG = "[SCAN-STATE-CACHE-CLEARER]"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + private static final QualifiedName STATE_HOLDER_KEY = + new QualifiedName(PLUGIN_ID, "state-holder"); + + private ScanStateCacheClearer() { + } + + /** + * Clear state cache for scanners that are being re-enabled. + * Allows manifest files to be re-scanned even if content hasn't changed. + * + * @param newlyEnabledScanners Scanners that just transitioned from disabled to enabled + */ + public static void clearForScanners(Set newlyEnabledScanners) { + if (newlyEnabledScanners == null || newlyEnabledScanners.isEmpty()) { + return; + } + + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (!project.isOpen()) { + continue; + } + try { + DevAssistScanStateHolder stateHolder = + (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + if (stateHolder == null) { + continue; + } + + // Clear ALL state cache entries to force fresh scans + // This ensures manifest files are re-scanned regardless of whether + // their content changed, since scanner enablement counts as "state changed" + stateHolder.clearAll(); + + CxLogger.info(LOG_TAG + " Cleared state cache for project: " + project.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing state cache for project " + + project.getName() + ": " + e.getMessage()); + } + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerMarkerPurger.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerMarkerPurger.java new file mode 100644 index 00000000..fa158b39 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerMarkerPurger.java @@ -0,0 +1,98 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; + +/** + * Purges findings for a scanner that has just been disabled. + * + * When a scanner is disabled, ScannerFactory stops running it for future scans, but + * results it already produced (cached ScanIssues, editor decorations, and IMarkers) + * remain until something removes them. This purges all three, workspace-wide, so a + * disabled scanner's findings disappear immediately. + */ +public class ScannerMarkerPurger { + + private static final String LOG_TAG = "[SCANNER-MARKER-PURGER]"; + private static final String MARKER_TYPE = "com.checkmarx.eclipse.plugin.checkmarxProblemMarker"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); + + private ScannerMarkerPurger() { + } + + /** + * Remove all markers, cached issues, and editor decorations produced by the given + * scanner, across every open project in the workspace. + * + * @param type Scanner type that was just disabled + */ + public static void purgeScanner(ScannerType type) { + if (type == null) { + return; + } + + String scannerName = type.name(); + purgeMarkers(scannerName); + purgeCacheAndDecorations(scannerName); + } + + private static void purgeMarkers(String scannerName) { + try { + IMarker[] markers = ResourcesPlugin.getWorkspace().getRoot() + .findMarkers(MARKER_TYPE, true, IResource.DEPTH_INFINITE); + int deleted = 0; + for (IMarker marker : markers) { + String engine = marker.getAttribute(MarkerIssueMapper.ATTR_SCAN_ENGINE, null); + if (scannerName.equals(engine)) { + marker.delete(); + deleted++; + } + } + CxLogger.info(LOG_TAG + " Deleted " + deleted + " markers for scanner: " + scannerName); + } catch (CoreException e) { + CxLogger.error(LOG_TAG + " Error deleting markers for scanner " + scannerName + ": " + e.getMessage(), e); + } + } + + private static void purgeCacheAndDecorations(String scannerName) { + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (!project.isOpen()) { + continue; + } + try { + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + if (problemHolder == null) { + continue; + } + + List affectedFiles = problemHolder.removeAllIssuesForScanner(scannerName); + for (String filePath : affectedFiles) { + IFile[] files = ResourcesPlugin.getWorkspace().getRoot() + .findFilesForLocation(org.eclipse.core.runtime.Path.fromOSString(filePath)); + IFile file = (files != null && files.length > 0) ? files[0] : null; + if (file != null) { + List remaining = + problemHolder.getScanIssuesByFile(filePath); + ProblemDecorator.decorateEditor(file, remaining); + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error purging cache for project " + project.getName() + ": " + e.getMessage()); + } + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerPreferencesListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerPreferencesListener.java new file mode 100644 index 00000000..22e83f7a --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerPreferencesListener.java @@ -0,0 +1,108 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; +import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; + +/** + * Listens for preference changes and syncs them to GlobalScannerController. + * + * This bridges the gap between CheckmarxPreferencePage (in common-lib) + * and GlobalScannerController (in devassist-lib) using the listener pattern + * to avoid circular module dependencies. + * + * Pattern from JetBrains: When preferences change, listeners update the + * runtime controller state so that scanner execution is gated by the + * latest preferences. + * + * Lifecycle: + * 1. User changes scanner checkboxes in CheckmarxPreferencePage + * 2. CheckmarxPreferencePage.performOk() saves to preferences + * 3. CheckmarxPreferencePage notifies ISettingsChangeNotifier + * 4. This listener's onSettingsApplied() is called + * 5. GlobalScannerController is synced with new preferences + * 6. Future scans respect the new preferences + */ +public class ScannerPreferencesListener implements ISettingsChangeNotifier { + + private static final String LOG_TAG = "[SCANNER-PREFS-LISTENER]"; + + /** + * Called when preferences are applied (from CheckmarxPreferencePage.performOk()). + * Syncs the preference store with GlobalScannerController so execution guards use latest state, + * then reacts to whatever changed: + * - Scanners that just got disabled have their existing findings purged immediately. + * - Scanners that just got enabled are combined into a single consolidated scan trigger, + * even if several scanners were toggled on at once in the same Apply/OK click. + */ + @Override + public void notifySettingsApplied() { + try { + CxLogger.info(LOG_TAG + " Syncing preferences to GlobalScannerController"); + + GlobalScannerController controller = GlobalScannerController.getInstance(); + + Map desiredState = new EnumMap<>(ScannerType.class); + desiredState.put(ScannerType.ASCA, Preferences.STORE.getBoolean(Preferences.PREF_ASCA_ENABLED)); + desiredState.put(ScannerType.OSS, Preferences.STORE.getBoolean(Preferences.PREF_OSS_ENABLED)); + desiredState.put(ScannerType.SECRETS, Preferences.STORE.getBoolean(Preferences.PREF_SECRETS_ENABLED)); + desiredState.put(ScannerType.CONTAINERS, Preferences.STORE.getBoolean(Preferences.PREF_CONTAINERS_ENABLED)); + desiredState.put(ScannerType.IAC, Preferences.STORE.getBoolean(Preferences.PREF_IAC_ENABLED)); + + CxLogger.info(LOG_TAG + " Read from STORE: " + desiredState); + + Set newlyEnabled = EnumSet.noneOf(ScannerType.class); + Set newlyDisabled = EnumSet.noneOf(ScannerType.class); + + for (Map.Entry entry : desiredState.entrySet()) { + ScannerType type = entry.getKey(); + boolean shouldBeEnabled = entry.getValue(); + boolean wasEnabled = controller.isScannerEnabled(type); + + if (shouldBeEnabled) { + controller.enableScanner(type); + } else { + controller.disableScanner(type); + } + + if (shouldBeEnabled && !wasEnabled) { + newlyEnabled.add(type); + } else if (!shouldBeEnabled && wasEnabled) { + newlyDisabled.add(type); + } + } + + CxLogger.info(LOG_TAG + " Preference sync complete. " + controller.getStateReport()); + + // Disable: purge findings for scanners that just got turned off. + for (ScannerType type : newlyDisabled) { + CxLogger.info(LOG_TAG + " Purging findings for disabled scanner: " + type); + ScannerMarkerPurger.purgeScanner(type); + } + + // Enable (single or multiple at once): clear state cache and trigger one consolidated scan. + if (!newlyEnabled.isEmpty()) { + CxLogger.info(LOG_TAG + " Clearing state cache for newly enabled scanners: " + newlyEnabled); + ScanStateCacheClearer.clearForScanners(newlyEnabled); + + CxLogger.info(LOG_TAG + " Triggering consolidated scan for newly enabled scanners: " + newlyEnabled); + IWorkspaceScanService scanService = Preferences.getWorkspaceScanService(); + if (scanService != null) { + scanService.scanWorkspace(); + } else { + CxLogger.warning(LOG_TAG + " No workspace scan service registered; cannot trigger scan"); + } + } + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to sync preferences: " + e.getMessage(), e); + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java new file mode 100644 index 00000000..2a56daf1 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java new file mode 100644 index 00000000..459da6f8 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxDocumentListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxDocumentListener.java new file mode 100644 index 00000000..647d52c8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxDocumentListener.java @@ -0,0 +1,106 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocumentListener; + +import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler; + +/** + * Real-time document listener for Checkmarx scanning. + * + * Equivalent to JetBrains' LocalInspectionTool.buildVisitor() — detects when + * the user edits the currently opened file and triggers a real-time scan with + * debounce (1 second of inactivity). + * + * This listener observes every keystroke and delegates to DevAssistScanScheduler + * for debounced scanning coordination. + */ +public class CheckmarxDocumentListener implements IDocumentListener { + + private final RealTimeScanJob scanJob; + private final IFile file; + private final String fileName; + private final DevAssistScanScheduler scheduler; + private volatile boolean skipNextChange = false; + private volatile long lastRescheduleTime = 0; + + /** + * Create a document listener for a specific file. + * + * @param fileName the name of the file being edited (for logging) + * @param scanJob the RealTimeScanJob to trigger on document changes + * @param file the IFile being edited + * @param scheduler the scheduler to coordinate scan rescheduling + */ + public CheckmarxDocumentListener(String fileName, RealTimeScanJob scanJob, IFile file, DevAssistScanScheduler scheduler) { + this.fileName = fileName; + this.scanJob = scanJob; + this.file = file; + this.scheduler = scheduler; + } + + /** + * Called when the document is about to be changed. + * We don't need to do anything here, but we implement it for completeness. + */ + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + // No action needed before change + } + + /** + * Called when the document has been changed. + * Triggers the debounced real-time scan via DevAssistScanScheduler. + * + * This is equivalent to JetBrains' InspectionVisitor methods being called + * during AST traversal — every edit triggers a potential scan. + */ + @Override + public void documentChanged(DocumentEvent event) { + try { + // Skip rescheduling if this is a programmatic change (e.g., annotation updates) + if (skipNextChange) { + skipNextChange = false; + return; + } + + // Prevent StackOverflowError from rapid recursive reschedules + long now = System.currentTimeMillis(); + if (now - lastRescheduleTime < 100) { + return; + } + lastRescheduleTime = now; + + // Reschedule the debounced scan job via scheduler + // This cancels the previous job (if still scheduled) and starts a new 1-second timer + if (scheduler != null && file != null) { + scheduler.rescheduleInspection(file, 1000); // 1000ms = 1 second debounce + } else if (scanJob != null) { + // Fallback to direct reschedule if scheduler not available + scanJob.reschedule(1000); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void setSkipNextChange(boolean skip) { + this.skipNextChange = skip; + } + + /** + * Dispose this listener and clean up associated resources. + * Call this when the editor is closed. + */ + public void dispose() { + if (scanJob != null) { + scanJob.cancel(); + } + } + + public String getFileName() { + return fileName; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java new file mode 100644 index 00000000..96cc6f5b --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java @@ -0,0 +1,425 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.jface.text.IDocument; +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) {} + + /** + * Trigger an immediate rescan of every currently open editor with real-time + * scanning set up. + * + * Called when scanner preferences change (e.g. ASCA/Secrets enabled) so files + * already open in editors are re-scanned right away, instead of waiting for + * the next keystroke or editor activation. + */ + public void rescanOpenEditors() { + for (RealTimeScanJob scanJob : activeScanJobs.values()) { + try { + scanJob.reschedule(0); + } catch (Exception e) { + System.err.println("[REALTIME] Error rescheduling scan job on preference change: " + e.getMessage()); + } + } + } + + /** + * Get the number of active listeners (for testing/debugging). + */ + public int getActiveListenerCount() { + return activeListeners.size(); + } + + /** + * Get the number of active scan jobs (for testing/debugging). + */ + public int getActiveScanJobCount() { + return activeScanJobs.size(); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java new file mode 100644 index 00000000..3dacde50 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -0,0 +1,371 @@ +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.common.utils.CxLogger; +import com.checkmarx.eclipse.common.listener.IProjectLifecycleListener; +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, IProjectLifecycleListener { + + 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(); + } + + /** + * Re-runs the workspace file scan for every open project, even ones already + * initialized. Called when scanner preferences change so newly-enabled scanners + * immediately produce results for files already covered by the workspace scan + * (manifests, IaC, container files), instead of waiting for the next project + * open/close event. + */ + @Override + public void rescanAllOpenProjects() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (IProject project : projects) { + if (!project.isOpen()) { + continue; + } + if (isInitialized(project)) { + startWorkspaceFileScanning(project); + } else { + onProjectOpen(project); + } + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error rescanning open projects: " + e.getMessage(), e); + } + } + + /** + * 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.common.preferences.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/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java new file mode 100644 index 00000000..87fd6c80 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java @@ -0,0 +1,240 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +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 is Scanning file : " + fileName); + this.file = file; + this.fileName = fileName; + + // Configure job properties for background execution + setSystem(false); // Show in progress view + setPriority(Job.DECORATE); // Lower priority than user interactions + setUser(false); // Not a user-initiated job + + + } + + /** + * Get the Eclipse log for this plugin. + */ + private ILog getLog() { + return Platform.getLog(getClass()); + } + + /** + * Reschedule this job with a given delay (debounce). + * + * If the job is already scheduled, it is cancelled and rescheduled with a new delay. + * This ensures the scan only runs after the user stops typing for the specified delay. + * + * @param delayMs delay in milliseconds before the job should run + */ + public synchronized void reschedule(long delayMs) { + // Update the last change time + this.lastChangeTime = System.currentTimeMillis(); + + // Cancel any previously scheduled execution + cancel(); + + // Schedule the job to run after the delay + schedule(delayMs); + + + } + + /** + * Run the real-time scan. + * + * This method is called by the Eclipse Jobs framework after the debounce delay expires. + * It performs the actual scanning logic. + * + * Currently, this just logs a message. In production, you would: + * 1. Parse the file + * 2. Run security checks (synchronously or via backend API) + * 3. Create markers for problems found + * 4. Update the editor decoration + * + * @param monitor progress monitor for cancellation support + * @return Status.OK if successful, Status.CANCEL if cancelled + */ + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + // Check if file still exists and is accessible + if (file == null || !file.exists()) { + + return Status.CANCEL_STATUS; + } + + // Check if the job was cancelled while waiting + if (monitor.isCanceled()) { + + return Status.CANCEL_STATUS; + } + + // **STEP 1: Check authentication status** + if (!isUserAuthenticated()) { + + + return Status.OK_STATUS; // Return OK but don't scan + } + + + + + + + // Call our backend scanners via ScanManager + try { + org.eclipse.core.resources.IProject project = file.getProject(); + if (project == null || !project.isOpen()) { + + return Status.OK_STATUS; + } + + String projectName = project.getName(); + org.eclipse.core.runtime.QualifiedName registryKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "scanner-registry"); + org.eclipse.core.runtime.QualifiedName stateHolderKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "state-holder"); + + // Get or lazily initialize backend services + com.checkmarx.eclipse.devassist.backend.ScannerRegistry registry = + (com.checkmarx.eclipse.devassist.backend.ScannerRegistry) + project.getSessionProperty(registryKey); + + com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder = + (com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder) + project.getSessionProperty(stateHolderKey); + + // Lazy initialization if not found + if (registry == null) { + + registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project); + project.setSessionProperty(registryKey, registry); + + } + + if (stateHolder == null) { + + stateHolder = new com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder(); + project.setSessionProperty(stateHolderKey, stateHolder); + + } + + // Execute backend scanners + + com.checkmarx.eclipse.devassist.common.ScanManager scanManager = + new com.checkmarx.eclipse.devassist.common.ScanManager(registry, stateHolder); + + String filePath = file.getLocation().toOSString(); + + + java.util.List issues = + scanManager.scanFile(filePath); + + + for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : issues) { + } + + // Publish results to UI + + if (!issues.isEmpty()) { + com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues); + + } else { + + } + + } catch (Exception e) { + System.err.println("[REALTIME] ✗ ERROR in step above: " + e.getMessage()); + e.printStackTrace(); + System.err.println("[REALTIME] Stack trace:"); + for (StackTraceElement elem : e.getStackTrace()) { + System.err.println("[REALTIME] at " + elem); + } + } + + + return Status.OK_STATUS; + + } catch (Exception e) { + System.err.println("[REALTIME] ✗ UNEXPECTED ERROR during real-time scan: " + e.getMessage()); + e.printStackTrace(); + System.err.println("[REALTIME] Full stack trace:"); + for (StackTraceElement elem : e.getStackTrace()) { + System.err.println("[REALTIME] at " + elem); + } + // Return error status but don't fail the job permanently + return new Status(IStatus.WARNING, "com.checkmarx.eclipse.plugin", + "Real-time scan failed for " + fileName, e); + } + } + + /** + * Check if user is authenticated by checking if API key is configured. + */ + private boolean isUserAuthenticated() { + String apiKey = com.checkmarx.eclipse.common.preferences.Preferences.getApiKey(); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + @Override + public boolean belongsTo(Object family) { + // Group all Checkmarx real-time scan jobs together + // This allows Eclipse to cancel all scans at once if needed + return family != null && family.equals("com.checkmarx.realtime.scan"); + } + + /** + * Called when the job is cancelled. + * Cleanup any resources if needed. + */ + @Override + protected void canceling() { + + super.canceling(); + } + + public String getFileName() { + return fileName; + } + + public IFile getFile() { + return file; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java new file mode 100644 index 00000000..8d671d8b --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java new file mode 100644 index 00000000..9a7c5ace --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java new file mode 100644 index 00000000..e7f7dbd3 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java new file mode 100644 index 00000000..27057f33 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java new file mode 100644 index 00000000..c1121b7f --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java new file mode 100644 index 00000000..ff9fe3f6 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanResult.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanResult.java new file mode 100644 index 00000000..1b7f240f --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java new file mode 100644 index 00000000..11a88458 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java new file mode 100644 index 00000000..a7a293d4 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java new file mode 100644 index 00000000..3b94af9f --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java @@ -0,0 +1,38 @@ +package com.checkmarx.eclipse.devassist.configuration; + +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; + +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Listens for authentication events and triggers MCP auto-installation. + * + * Registered globally to respond to successful authentication by: + * - Detecting API_KEY changes in preferences + * - Triggering MCP configuration installation + * - Logging success/failure for debugging + */ +public class AuthenticationListener implements IPropertyChangeListener { + + private static final String LOG_TAG = "[AUTH-LISTENER]"; + + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event == null || event.getProperty() == null) { + return; + } + + // Trigger MCP auto-install when API key is successfully set + if (Preferences.API_KEY.equals(event.getProperty())) { + String newApiKey = (String) event.getNewValue(); + + // Only proceed if a key was set (not cleared) + if (newApiKey != null && !newApiKey.isBlank()) { + CxLogger.info(LOG_TAG + " API key updated, attempting MCP auto-install..."); + McpInstallService.attemptAutoInstall(); + } + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationSuccessHandler.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationSuccessHandler.java new file mode 100644 index 00000000..d0ae79f0 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationSuccessHandler.java @@ -0,0 +1,60 @@ +package com.checkmarx.eclipse.devassist.configuration; + +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Display; + +import com.checkmarx.eclipse.common.listener.IAuthenticationSuccessHandler; +import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.ui.preferences.WelcomeDialog; + +/** + * Handles post-authentication UI and backend setup in devassist-lib. + * + * Triggered when authentication succeeds in PreferencesPage, this handler: + * - Shows the welcome dialog + * - Re-enables the logout button + * - Ensures workspace scans are triggered for open projects + */ +public class AuthenticationSuccessHandler implements IAuthenticationSuccessHandler { + + private static final String LOG_TAG = "[AUTH-SUCCESS]"; + + @Override + public void onAuthenticationSuccess(boolean mcpEnabled, Object logoutButton, String apiKey, String additionalParams) { + try { + Button logout = (Button) logoutButton; + + // Trigger workspace scan via service (avoids importing PluginStartup in devassist-lib) + IWorkspaceScanService scanService = Preferences.getWorkspaceScanService(); + if (scanService != null) { + scanService.scanWorkspace(); + } else { + CxLogger.warning(LOG_TAG + " Workspace scan service not available"); + } + + // Show welcome dialog with MCP status + WelcomeDialog dlg = new WelcomeDialog( + Display.getDefault().getActiveShell(), + mcpEnabled); + + // Re-enable Logout right as the welcome dialog is about to appear, so it stays + // disabled for the entire connect/validate flow and only becomes usable once + // that flow has visibly completed. + if (logout != null && !logout.isDisposed()) { + logout.setEnabled(true); + } + + dlg.open(); + } catch (Exception ex) { + CxLogger.error(LOG_TAG + " Failed to show welcome dialog", ex); + if (logoutButton != null && logoutButton instanceof Button) { + Button btn = (Button) logoutButton; + if (!btn.isDisposed()) { + btn.setEnabled(true); + } + } + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java new file mode 100644 index 00000000..f700688c --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java @@ -0,0 +1,196 @@ +package com.checkmarx.eclipse.devassist.configuration; + +import java.util.concurrent.CompletableFuture; + +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.runner.TenantSettingsProvider; +import com.checkmarx.eclipse.common.utils.CxLogger; + + +/** + * MCP Installation Service for Eclipse plugin. + * + * Responsible for: + * - Auto-installing MCP configuration on plugin startup + * - Validating authentication and MCP tenant settings + * - Asynchronous background MCP setup + * - Comprehensive logging + * + * Follows the JetBrains implementation pattern for consistency. + */ +public final class McpInstallService { + + private static final String LOG_TAG = "[MCP-INSTALL]"; + private static boolean authListenerRegistered = false; + + private McpInstallService() { + // Utility class + } + + static { + // Register authentication handlers on class load + registerAuthenticationHandlers(); + } + + private static void registerAuthenticationHandlers() { + if (!authListenerRegistered) { + // Register listener for MCP auto-install on API key change + Preferences.STORE.addPropertyChangeListener(new AuthenticationListener()); + + // Register handler for post-authentication UI (welcome dialog, workspace scan) + Preferences.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler()); + + authListenerRegistered = true; + CxLogger.info(LOG_TAG + " Authentication handlers registered"); + } + } + + /** + * Conditionally installs MCP configuration if user is authenticated + * and MCP is enabled for their tenant. + * + * Conditions checked: + * - User is authenticated (API key configured) + * - AI MCP server flag is enabled in tenant settings + * - A credential token is available + * + * If any condition fails, installation is silently skipped. + */ + public static void attemptAutoInstall() { + CxLogger.info(LOG_TAG + " Attempting auto-install of MCP configuration..."); + + try { + String apiKey = Preferences.getApiKey(); + String additionalParams = Preferences.getAdditionalOptions(); + + if (apiKey == null || apiKey.isBlank()) { + CxLogger.info(LOG_TAG + " Skipping MCP auto-install: user not authenticated (no API key)"); + return; + } + + attemptAutoInstall(apiKey, additionalParams); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Unexpected error during auto-install attempt: " + e.getMessage(), e); + } + } + + /** + * Conditionally installs MCP configuration with provided credentials. + * + * Used when API key is freshly authenticated but not yet persisted to preferences. + * Same conditions as attemptAutoInstall() but accepts credentials as parameters. + * + * @param apiKey API key from authentication (may not be persisted yet) + * @param additionalParams Additional params for Checkmarx API + */ + public static void attemptAutoInstall(String apiKey, String additionalParams) { + CxLogger.info(LOG_TAG + " Attempting auto-install of MCP configuration..."); + + try { + if (apiKey == null || apiKey.isBlank()) { + CxLogger.info(LOG_TAG + " Skipping MCP auto-install: user not authenticated (no API key)"); + return; + } + + CxLogger.info(LOG_TAG + " User is authenticated, checking MCP server flag..."); + + // Check if MCP is enabled for tenant + boolean aiMcpEnabled; + try { + aiMcpEnabled = TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled(apiKey, additionalParams); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to check MCP server status, skipping: " + e.getMessage()); + return; + } + + if (!aiMcpEnabled) { + CxLogger.info(LOG_TAG + " Skipping MCP auto-install: AI MCP server disabled for tenant"); + return; + } + + CxLogger.info(LOG_TAG + " ✓ All conditions met, installing MCP asynchronously..."); + + // Install in background without blocking + installSilentlyAsync(apiKey); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Unexpected error during auto-install attempt: " + e.getMessage(), e); + } +} + + /** + * Asynchronously installs MCP configuration without user notifications. + * Failures are logged but do not interrupt plugin startup. + * + * @param credential API key for Copilot MCP Authorization header + * @return future resolving to Boolean (true=changed, false=unchanged, null=error) + */ + public static CompletableFuture installSilentlyAsync(String credential) { + if (credential == null || credential.isBlank()) { + CxLogger.info(LOG_TAG + " Cannot install: credential is null or empty"); + return CompletableFuture.completedFuture(false); + } + + return CompletableFuture.supplyAsync(() -> { + try { + CxLogger.info(LOG_TAG + " Background thread started, installing MCP..."); + boolean changed = McpSettingsInjector.installForCopilot(credential); + + if (changed) { + CxLogger.info(LOG_TAG + " ✓ MCP installation completed successfully (config modified)"); + } else { + CxLogger.info(LOG_TAG + " MCP installation completed (config unchanged)"); + } + + return changed; + } catch (Throwable ex) { + // Catches Throwable, not just Exception: a class-loading failure (e.g. + // NoClassDefFoundError/LinkageError) inside McpSettingsInjector is an Error, + // which a plain "catch (Exception)" would miss - and since this future is + // never joined/observed by the caller, an uncaught Error here would otherwise + // vanish silently with no log at all. + logBackgroundFailure(ex); + return null; // null signals failure + } + }).exceptionally(ex -> { + // Safety net in case something fails outside the try/catch above + // (e.g. the executor itself, or the catch block's own logging call). + logBackgroundFailure(ex); + return null; + }); + } + + /** + * Logs a background MCP installation failure, preserving the original + * stack trace even when the failure is an Error rather than an Exception. + */ + private static void logBackgroundFailure(Throwable ex) { + String msg = LOG_TAG + " Background MCP installation failed: " + ex.getClass().getName() + ": " + ex.getMessage(); + Exception loggable = (ex instanceof Exception) ? (Exception) ex : new RuntimeException(ex); + CxLogger.error(msg, loggable); + } + + /** + * Uninstalls MCP configuration. Called during plugin cleanup. + * + * @return true if MCP entry was removed, false if not found + */ + public static boolean uninstall() { + CxLogger.info(LOG_TAG + " Uninstalling MCP configuration..."); + + try { + boolean removed = McpSettingsInjector.uninstallFromCopilot(); + + if (removed) { + CxLogger.info(LOG_TAG + " ✓ MCP configuration uninstalled successfully"); + } else { + CxLogger.info(LOG_TAG + " No MCP configuration found to uninstall"); + } + + return removed; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to uninstall MCP: " + e.getMessage(), e); + return false; + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java new file mode 100644 index 00000000..c73d06d0 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java @@ -0,0 +1,322 @@ +package com.checkmarx.eclipse.devassist.configuration; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.osgi.service.prefs.BackingStoreException; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Injects Checkmarx MCP server configuration into GitHub Copilot for Eclipse. + * + *

+ * GitHub Copilot for Eclipse (https://github.com/microsoft/copilot-for-eclipse) + * reads its MCP server list from an Eclipse {@code IEclipsePreferences} node + * scoped to its UI bundle ({@code com.microsoft.copilot.eclipse.ui}), under the + * preference key {@code "mcp"} (see {@code LanguageServerSettingManager + * #syncMcpRegistrationConfiguration}, which calls + * {@code preferenceStore.getString(Constants.MCP)}). The value is a JSON string + * containing either {@code {"servers": {...}}} or a bare + * {@code {"name": {...}}} map, using the same schema as VS Code's + * {@code mcp.json} (the plugin embeds the same Copilot language server used by + * VS Code). At the time of writing, Copilot for Eclipse does not yet read a + * file-based {@code mcp.json} (that support is still an open, unmerged + * proposal - microsoft/copilot-for-eclipse#127/#128), so the preference store + * is the only mechanism that actually works against released builds. + * + *

+ * Writing directly to this preference node (rather than through Copilot's own + * API, which this plugin does not depend on) is safe and immediate: Copilot's + * own {@code ScopedPreferenceStore} listens on the same underlying node, so our + * write is picked up live and re-synced to the language server without + * requiring a restart. + * + *

+ * Responsible for: + *

    + *
  • Merging/removing the Checkmarx MCP server entry in Copilot's "mcp" + * preference, preserving any other servers already configured there
  • + *
  • Token validation and URL derivation
  • + *
  • Logging all operations with aggressive debug info
  • + *
+ */ +public final class McpSettingsInjector { + + private static final String LOG_TAG = "[MCP-INJECTOR]"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String FALLBACK_BASE = "https://ast-master-components.dev.cxast.net"; + private static final String SERVER_KEY = "checkmarx"; + public static final String MCP_ENDPOINT = "/api/security-mcp/mcp"; + + /** Bundle symbolic name of GitHub Copilot for Eclipse's UI plugin. */ + private static final String COPILOT_UI_BUNDLE_ID = "com.microsoft.copilot.eclipse.ui"; + + /** Preference key Copilot reads its MCP server JSON from (Constants.MCP). */ + private static final String MCP_PREFERENCE_KEY = "mcp"; + + private McpSettingsInjector() { + // Utility class + } + + /** + * Installs/updates Checkmarx MCP configuration for Copilot. + * + * @param token API key or JWT token with issuer claim + * @return true if config was modified, false if already up-to-date + * @throws Exception if installation fails + */ + public static boolean installForCopilot(String token) throws Exception { + CxLogger.info(LOG_TAG + " Starting MCP installation for Copilot..."); + + if (token == null || token.isBlank()) { + CxLogger.warning(LOG_TAG + " Cannot install MCP: token is null or empty"); + return false; + } + + try { + String issuer = tryExtractIssuer(token); + CxLogger.info(LOG_TAG + " Token issuer extracted: " + (issuer != null ? issuer : "null (using fallback)")); + + String baseUrl = deriveBaseUrlFromIssuer(issuer); + CxLogger.info(LOG_TAG + " Derived base URL: " + baseUrl); + + String mcpUrl = baseUrl + MCP_ENDPOINT; + CxLogger.info(LOG_TAG + " MCP URL: " + mcpUrl); + + CxLogger.info(LOG_TAG + " Copilot MCP preference node: " + COPILOT_UI_BUNDLE_ID + " / " + MCP_PREFERENCE_KEY); + + boolean changed = mergeCheckmarxServer(mcpUrl, token); + + if (changed) { + CxLogger.info(LOG_TAG + " MCP configuration installed/updated successfully"); + } else { + CxLogger.info(LOG_TAG + " MCP configuration unchanged (already up-to-date)"); + } + + return changed; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to install MCP configuration: " + e.getMessage(), e); + throw e; + } + } + + /** + * Uninstalls Checkmarx MCP server entry from Copilot configuration. + * + * @return true if entry was removed, false if not found + * @throws Exception if uninstallation fails + */ + public static boolean uninstallFromCopilot() throws Exception { + CxLogger.info(LOG_TAG + " Starting MCP uninstallation..."); + + try { + boolean removed = removeCheckmarxServer(); + + if (removed) { + CxLogger.info(LOG_TAG + " Checkmarx MCP entry removed successfully"); + } else { + CxLogger.info(LOG_TAG + " No Checkmarx MCP entry found to remove"); + } + + return removed; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to uninstall MCP configuration: " + e.getMessage(), e); + throw e; + } + } + + /** + * Merges the Checkmarx server entry into Copilot's "mcp" preference, keeping + * any other servers already present. Returns true if the preference value was + * modified, false if content unchanged. + */ + private static boolean mergeCheckmarxServer(String url, String token) throws BackingStoreException { + IEclipsePreferences node = InstanceScope.INSTANCE.getNode(COPILOT_UI_BUNDLE_ID); + + CxLogger.info(LOG_TAG + " Reading existing Copilot MCP preference..."); + Map servers = readServers(node); + + Map headers = new LinkedHashMap<>(); + headers.put("cx-origin", "eclipse-plugin"); + headers.put("Authorization", token); + + Map serverEntry = new LinkedHashMap<>(); + serverEntry.put("type", "http"); + serverEntry.put("url", url); + serverEntry.put("headers", headers); + + Object existing = servers.get(SERVER_KEY); + boolean changed = !Objects.equals(existing, serverEntry); + CxLogger.info(LOG_TAG + " Config changed: " + changed); + + if (!changed) { + CxLogger.info(LOG_TAG + " Existing MCP entry matches new entry exactly"); + return false; + } + + CxLogger.info(LOG_TAG + " Updating MCP server entry in Copilot preference"); + servers.put(SERVER_KEY, serverEntry); + writeServers(node, servers); + + CxLogger.info(LOG_TAG + " MCP preference updated for bundle: " + COPILOT_UI_BUNDLE_ID); + return true; + } + + /** + * Removes the Checkmarx server entry from Copilot's "mcp" preference. Returns + * true if the entry was removed, false if not found. + */ + private static boolean removeCheckmarxServer() throws BackingStoreException { + IEclipsePreferences node = InstanceScope.INSTANCE.getNode(COPILOT_UI_BUNDLE_ID); + + CxLogger.info(LOG_TAG + " Reading Copilot MCP preference for removal..."); + Map servers = readServers(node); + + boolean removed = servers.remove(SERVER_KEY) != null; + + if (!removed) { + CxLogger.info(LOG_TAG + " Checkmarx MCP entry not found in Copilot preference"); + return false; + } + + CxLogger.info(LOG_TAG + " Checkmarx MCP entry found and removed"); + writeServers(node, servers); + + CxLogger.info(LOG_TAG + " MCP entry removed from bundle preference: " + COPILOT_UI_BUNDLE_ID); + return true; + } + + /** + * Reads the "mcp" preference value and extracts the servers map. Accepts both + * {@code {"servers": {...}}} and bare {@code {"name": {...}}} forms (mirroring + * how Copilot itself parses this preference), tolerating a blank or invalid + * value by returning an empty, mutable map. + */ + @SuppressWarnings("unchecked") + private static Map readServers(IEclipsePreferences node) { + String raw = node.get(MCP_PREFERENCE_KEY, ""); + if (raw == null || raw.isBlank()) { + CxLogger.info(LOG_TAG + " No existing Copilot MCP preference value, starting fresh"); + return new LinkedHashMap<>(); + } + + try { + Map parsed = MAPPER.readValue(raw, new TypeReference>() { + }); + if (parsed == null) { + return new LinkedHashMap<>(); + } + + Object serversObj = parsed.get("servers"); + if (serversObj instanceof Map) { + CxLogger.info(LOG_TAG + "Existing preference read successfully (wrapped form)"); + return new LinkedHashMap<>((Map) serversObj); + } + + CxLogger.info(LOG_TAG + "Existing preference read successfully (bare form)"); + return new LinkedHashMap<>(parsed); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to parse existing Copilot MCP preference, starting fresh: " + e.getMessage()); + return new LinkedHashMap<>(); + } + } + + /** + * Writes the servers map back to the "mcp" preference, wrapped as + * {@code {"servers": {...}}}, and flushes it so it is persisted immediately + * and observed by Copilot's live preference listeners. + */ + private static void writeServers(IEclipsePreferences node, Map servers) throws BackingStoreException { + try { + if (servers.isEmpty()) { + node.remove(MCP_PREFERENCE_KEY); + } else { + Map root = new LinkedHashMap<>(); + root.put("servers", servers); + node.put(MCP_PREFERENCE_KEY, MAPPER.writeValueAsString(root)); + } + node.flush(); + } catch (BackingStoreException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to serialize Copilot MCP preference", e); + } + } + + /** + * Extracts the issuer claim from a JWT token. + * Token format: header.payload.signature + * Payload is base64url encoded JSON containing "iss" claim. + */ + private static String tryExtractIssuer(String rawToken) { + if (rawToken == null || rawToken.isBlank()) { + CxLogger.info(LOG_TAG + " Token is null or empty"); + return null; + } + + try { + String[] parts = rawToken.split("\\."); + if (parts.length < 2) { + CxLogger.info(LOG_TAG + " Token does not have expected JWT format (parts=" + parts.length + ")"); + return null; + } + + CxLogger.info(LOG_TAG + " Decoding JWT payload..."); + byte[] payload = Base64.getUrlDecoder().decode(parts[1]); + String json = new String(payload, StandardCharsets.UTF_8); + + Map map = MAPPER.readValue(json, new TypeReference>() { + }); + Object iss = map.get("iss"); + + if (iss != null) { + CxLogger.info(LOG_TAG + "Issuer extracted: " + iss.toString()); + return iss.toString(); + } + + CxLogger.info(LOG_TAG + " No 'iss' claim found in JWT payload"); + return null; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to parse JWT token: " + e.getMessage()); + return null; + } + } + + /** + * Derives AST base URL from issuer claim. + * If issuer is like https://iam.checkmarx.com, converts to https://ast.checkmarx.com + */ + private static String deriveBaseUrlFromIssuer(String issuer) { + if (issuer == null || issuer.isBlank()) { + CxLogger.info(LOG_TAG + " Issuer is null/empty, using fallback base URL"); + return FALLBACK_BASE; + } + + try { + CxLogger.info(LOG_TAG + " Deriving base URL from issuer: " + issuer); + String host = URI.create(issuer).getHost(); + + if (host != null && host.contains("iam.checkmarx")) { + String newHost = host.replace("iam", "ast"); + String baseUrl = "https://" + newHost; + CxLogger.info(LOG_TAG + "Derived base URL: " + baseUrl); + return baseUrl; + } + + CxLogger.info(LOG_TAG + " Host does not match iam.checkmarx pattern, using fallback"); + return FALLBACK_BASE; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to derive base URL from issuer: " + e.getMessage()); + return FALLBACK_BASE; + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java new file mode 100644 index 00000000..bc7f9abd --- /dev/null +++ b/devassist-lib/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.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.Constants; +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(Constants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); + throw new Exception(e); + } + + return cxWrapper; + } +} \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java new file mode 100644 index 00000000..8dbd41d1 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java new file mode 100644 index 00000000..2d9feb94 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java new file mode 100644 index 00000000..94fa5a10 --- /dev/null +++ b/devassist-lib/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.backend.listener.RealTimeScanJob; +import com.checkmarx.eclipse.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/model/Location.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/model/Location.java new file mode 100644 index 00000000..7e010a9d --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java new file mode 100644 index 00000000..6b7d6fbb --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java new file mode 100644 index 00000000..f72d961b --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java new file mode 100644 index 00000000..7befff35 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java @@ -0,0 +1,129 @@ +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; + private String expectedValue; + private String remediationAdvise; // Fix suggestion, if available + private String SimilarityId; + private String problematicLine; + private Integer ruleId; + + 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; + } + + public String getExpectedValue() { + return expectedValue; + } + + public void setExpectedValue(String expectedValue) { + this.expectedValue = expectedValue; + } + + public String getRemediationAdvise() { + return remediationAdvise; + } + + public void setRemediationAdvise(String remediationAdvise) { + this.remediationAdvise = remediationAdvise; + } + + public String getSimilarityId() { + return SimilarityId; + } + + public void setSimilarityId(String similarityId) { + SimilarityId = similarityId; + } + + public String getProblematicLine() { + return problematicLine; + } + + public void setProblematicLine(String problematicLine) { + this.problematicLine = problematicLine; + } + + public Integer getRuleId() { + return ruleId; + } + + public void setRuleId(Integer ruleId) { + this.ruleId = ruleId; + } + + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java new file mode 100644 index 00000000..8b047444 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java new file mode 100644 index 00000000..28947d48 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -0,0 +1,668 @@ +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.Position; +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.common.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); + // **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 + 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) { + // 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 + "COMPLETE: Added " + annotations.size() + + " annotations to editor"); + + } 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 + */ + /** + * Decorate the complete OSS dependency block using the first and last + * locations from the issue. + * + * For OSS vulnerabilities, the Locations array contains the line/range + * information for the complete dependency block. The decoration starts + * from the first location's StartIndex and ends at the last location's + * EndIndex. + * + * Leading whitespace before the first StartIndex and trailing whitespace + * after the last EndIndex are not decorated. + * + * @param editor Text editor + * @param issue OSS issue + * @return Position covering the complete OSS dependency block, or null if unable to determine + */ + private static Position decorateOssFirstLineOnly(ITextEditor editor, ScanIssue issue) { + + try { + IDocument document = editor.getDocumentProvider() + .getDocument(editor.getEditorInput()); + + if (document == null) { + CxLogger.warning(LOG_TAG + " [OSS] Document is null!"); + return null; + } + + // Get locations from issue + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + CxLogger.warning(LOG_TAG + " [OSS] No locations found!"); + return null; + } + + Location firstLocation = issue.getLocations().get(0); + Location lastLocation = issue.getLocations().get(issue.getLocations().size() - 1); + + // Convert line numbers from 1-based to 0-based + int firstLineNumber = firstLocation.getLine() - 1; + int lastLineNumber = lastLocation.getLine() - 1; + + int lineCount = document.getNumberOfLines(); + int docLength = document.getLength(); + + // Bounds check + if (firstLineNumber < 0 || firstLineNumber >= lineCount) { + CxLogger.warning(LOG_TAG + " [OSS] First line " + (firstLineNumber + 1) + " out of bounds (doc has " + + lineCount + " lines)"); + return null; + } + + if (lastLineNumber < 0 || lastLineNumber >= lineCount) { + CxLogger.warning(LOG_TAG + " [OSS] Last line " + (lastLineNumber + 1) + " out of bounds (doc has " + + lineCount + " lines)"); + return null; + } + + IRegion firstLineInfo = document.getLineInformation(firstLineNumber); + IRegion lastLineInfo = document.getLineInformation(lastLineNumber); + int firstLineOffset = firstLineInfo.getOffset(); + int lastLineOffset = lastLineInfo.getOffset(); + int firstLineLength = firstLineInfo.getLength(); + int lastLineLength = lastLineInfo.getLength(); + int startIndex = firstLocation.getStartIndex(); + int endIndex = lastLocation.getEndIndex(); + startIndex = Math.max(0, Math.min(startIndex, firstLineLength)); + endIndex = Math.max(0, Math.min(endIndex, lastLineLength)); + + int startOffset = firstLineOffset + startIndex; + int endOffset = lastLineOffset + endIndex; + + while (startOffset < endOffset && startOffset < docLength + && Character.isWhitespace(document.getChar(startOffset))) { + startOffset++; + } + while (endOffset > startOffset && endOffset <= docLength + && Character.isWhitespace(document.getChar(endOffset - 1))) { + endOffset--; + } + + if (startOffset < 0 || startOffset > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Invalid start offset: " + startOffset); + return null; + } + + if (endOffset < startOffset || endOffset > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Invalid end offset: " + endOffset); + return null; + } + + int decorationLength = endOffset - startOffset; + + if (decorationLength <= 0) { + CxLogger.warning(LOG_TAG + " [OSS] Invalid decoration length: " + decorationLength); + return null; + } + + CxLogger.info(LOG_TAG + " [OSS] Decorating dependency block"); + + CxLogger.info(LOG_TAG + " [OSS] First line: " + (firstLineNumber + 1) + ", StartIndex: " + + firstLocation.getStartIndex()); + + CxLogger.info(LOG_TAG + " [OSS] Last line: " + (lastLineNumber + 1) + ", EndIndex: " + + lastLocation.getEndIndex()); + + CxLogger.info(LOG_TAG + " [OSS] Final Position: [" + startOffset + "-" + endOffset + "] = " + + decorationLength + " chars"); + + return new Position(startOffset, decorationLength); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " [OSS] Error decorating dependency block: " + 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 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/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java new file mode 100644 index 00000000..dee7d2a3 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java new file mode 100644 index 00000000..19e9717b --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java new file mode 100644 index 00000000..037c732d --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -0,0 +1,309 @@ +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 org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * 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)"); + } + + /** + * Remove cached scan issues for a scanner across ALL files in this project. + * Used when a scanner is disabled and its findings must be purged immediately. + * + * @param scannerType Name of the scanner engine (e.g., "ASCA", "OSS", "IAC") + * @return the file paths that had at least one issue removed, so callers can + * refresh editor decorations/markers for those files + */ + public List removeAllIssuesForScanner(String scannerType) { + List affectedFiles = new ArrayList<>(); + if (scannerType == null) { + return affectedFiles; + } + + for (Map.Entry> entry : fileToScanIssues.entrySet()) { + boolean hasMatch = entry.getValue().stream() + .anyMatch(issue -> issue.getScanEngine() != null && issue.getScanEngine().name().equals(scannerType)); + if (hasMatch) { + affectedFiles.add(entry.getKey()); + } + } + + for (String filePath : affectedFiles) { + removeScanIssuesByFileAndScanner(scannerType, filePath); + } + + if (!affectedFiles.isEmpty()) { + publishIssuesUpdated(); + } + + return affectedFiles; + } + + /** + * 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 = (IEventBroker) PlatformUI.getWorkbench().getService(IEventBroker.class); + 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/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java new file mode 100644 index 00000000..405fcda6 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotInstallNotificationPopup.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotInstallNotificationPopup.java new file mode 100644 index 00000000..6e6947cf --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotInstallNotificationPopup.java @@ -0,0 +1,63 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Notification popup prompting the user to install GitHub Copilot for Eclipse, with a + * clickable link that opens its Eclipse Marketplace listing in the system browser. + *

+ * Unlike {@link NotificationPopup}, this popup does not auto-close: installing a plugin + * takes the user out of Eclipse, so it stays visible (with its standard close control) until + * dismissed. + */ +public class CopilotInstallNotificationPopup extends AbstractNotificationPopup { + + private final String title; + private final String message; + private final String marketplaceUrl; + + public CopilotInstallNotificationPopup(Display display, String title, String message, String marketplaceUrl) { + super(display); + this.title = title; + this.message = message; + this.marketplaceUrl = marketplaceUrl; + } + + @Override + protected void createContentArea(Composite parent) { + Label label = new Label(parent, SWT.WRAP); + label.setText(message); + label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Link link = new Link(parent, SWT.NONE); + link.setText("Open Eclipse Marketplace"); + link.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + link.addListener(SWT.Selection, event -> openMarketplace()); + } + + private void openMarketplace() { + try { + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(marketplaceUrl)); + } catch (PartInitException | MalformedURLException e) { + CxLogger.error("Failed to open Eclipse Marketplace link: " + e.getMessage(), e); + } + } + + @Override + protected String getPopupShellTitle() { + return title; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotIntegration.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotIntegration.java new file mode 100644 index 00000000..f9d78d9c --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotIntegration.java @@ -0,0 +1,184 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.runtime.Platform; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.commands.ICommandService; +import org.osgi.framework.Bundle; + +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Integration with GitHub Copilot for Eclipse + * (https://github.com/microsoft/copilot-for-eclipse). + *

+ * Opens the Copilot Chat view in Agent mode with a pre-filled prompt and + * submits it automatically, using the command contributed by the Copilot + * plugin: {@code com.microsoft.copilot.eclipse.commands.openChatView}. + *

+ * Fallback strategy, in order: + *

    + *
  1. If GitHub Copilot for Eclipse is not installed, a notification is shown + * inviting the user to install it from the Eclipse Marketplace.
  2. + *
  3. If Copilot is installed but the command could not be invoked (e.g. + * disabled, or the command contract changed in a future Copilot release), the + * prompt is copied to the clipboard.
  4. + *
+ * In both fallback cases the prompt is always copied to the clipboard and a + * balloon notification confirms it, so the user never loses the generated + * prompt. + */ +public final class CopilotIntegration { + + private static final String LOG_PREFIX = "[CX-COPILOT-INTEGRATION]"; + + /** + * Bundle symbolic ids used to detect whether GitHub Copilot for Eclipse is + * installed. Checking both the core and UI bundles guards against internal + * repackaging. + */ + private static final String[] COPILOT_BUNDLE_IDS = { "com.microsoft.copilot.eclipse.core", + "com.microsoft.copilot.eclipse.ui" }; + + /** Command contributed by GitHub Copilot for Eclipse to open the chat view. */ + private static final String COPILOT_OPEN_CHAT_COMMAND = "com.microsoft.copilot.eclipse.commands.openChatView"; + + /** Initial text to place in the chat input. */ + private static final String PARAM_INPUT_VALUE = "com.microsoft.copilot.eclipse.commands.openChatView.inputValue"; + + /** Whether the chat input should be submitted automatically once set. */ + private static final String PARAM_AUTO_SEND = "com.microsoft.copilot.eclipse.commands.openChatView.autoSend"; + + /** Chat mode to switch to before submitting ("Agent" or "Ask"). */ + private static final String PARAM_MODE = "com.microsoft.copilot.eclipse.commands.openChatView.mode"; + + private static final String CHAT_MODE_AGENT = "Agent"; + + private static final String COPILOT_MARKETPLACE_URL = "https://marketplace.eclipse.org/content/github-copilot"; + + private static final String INSTALL_NOTIFICATION_TITLE = "GitHub Copilot for Eclipse Not Installed"; + private static final String INSTALL_NOTIFICATION_MESSAGE = "GitHub Copilot for Eclipse is required to fix the vulnerability.\nInstall it from the Eclipse Marketplace, then try again."; + + private CopilotIntegration() { + throw new IllegalStateException("Cannot instantiate CopilotIntegration class"); + } + + /** + * Opens GitHub Copilot Chat in Agent mode with the given prompt and submits it + * automatically. + *

+ * If Copilot is not installed, an "install Copilot" notification is shown. In + * every case where the prompt could not be handed off to Copilot directly, it + * is copied to the clipboard and a confirmation balloon is shown. + * + * @param prompt the prompt to send to Copilot + * @return true if the prompt was successfully handed off to Copilot or copied + * to the clipboard as a fallback; false only if the prompt itself is + * invalid + */ + public static boolean sendPromptToCopilot(String prompt) { + if (prompt == null || prompt.isEmpty()) { + CxLogger.error(LOG_PREFIX + " Cannot send an empty prompt to Copilot", + new Exception("Empty prompt for Copilot")); + return false; + } + + if (!isCopilotInstalled()) { + CxLogger.warning(LOG_PREFIX + " GitHub Copilot for Eclipse is not installed"); + showInstallCopilotNotification(); + return false; + } + + if (openChatInAgentModeAndSend(prompt)) { + CxLogger.info(LOG_PREFIX + " Prompt sent to Copilot Chat in Agent mode and submitted automatically"); + return true; + } + + CxLogger.warning(LOG_PREFIX + " Could not invoke Copilot's open chat command - falling back to clipboard"); + return false; + } + + /** + * Checks whether GitHub Copilot for Eclipse is installed in this IDE instance. + * + * @return true if the Copilot plugin's bundles are present + */ + public static boolean isCopilotInstalled() { + for (String bundleId : COPILOT_BUNDLE_IDS) { + Bundle bundle = Platform.getBundle(bundleId); + if (bundle != null && bundle.getState() != Bundle.UNINSTALLED) { + return true; + } + } + return false; + } + + /** + * Executes the Copilot {@code openChatView} command, switching to Agent mode, + * pre-filling the prompt, and requesting an automatic submit. + * + * @param prompt the prompt to place in the chat input + * @return true if the command was found, enabled, and executed without error + */ + private static boolean openChatInAgentModeAndSend(String prompt) { + final boolean[] success = { false }; + + try { + Display.getDefault().syncExec(() -> { + try { + ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); + if (commandService == null) { + CxLogger.warning(LOG_PREFIX + " ICommandService not available"); + return; + } + + Command command = commandService.getCommand(COPILOT_OPEN_CHAT_COMMAND); + if (command == null || !command.isDefined()) { + CxLogger.warning(LOG_PREFIX + " Copilot command not defined: " + COPILOT_OPEN_CHAT_COMMAND); + return; + } + + if (!command.isEnabled()) { + CxLogger.warning(LOG_PREFIX + " Copilot command is not enabled: " + COPILOT_OPEN_CHAT_COMMAND); + return; + } + + // The handler reads these as raw command parameters (always Strings) - + // passing a real Boolean here throws a ClassCastException in Copilot's handler. + Map parameters = new HashMap<>(); + parameters.put(PARAM_INPUT_VALUE, prompt); + parameters.put(PARAM_AUTO_SEND, Boolean.TRUE.toString()); + parameters.put(PARAM_MODE, CHAT_MODE_AGENT); + + command.executeWithChecks(new ExecutionEvent(command, parameters, null, null)); + success[0] = true; + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error executing Copilot open chat command: " + e.getMessage()); + } + }); + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Unexpected error opening Copilot Chat: " + e.getMessage(), e); + } + + return success[0]; + } + + + /** + * Shows a notification prompting the user to install GitHub Copilot for + * Eclipse, with a link to its Eclipse Marketplace listing. + */ + private static void showInstallCopilotNotification() { + Display.getDefault().asyncExec(() -> { + Display display = Display.getDefault(); + CopilotInstallNotificationPopup popup = new CopilotInstallNotificationPopup(display, + INSTALL_NOTIFICATION_TITLE, INSTALL_NOTIFICATION_MESSAGE, COPILOT_MARKETPLACE_URL); + popup.open(); + }); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java new file mode 100644 index 00000000..b5c34403 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java @@ -0,0 +1,661 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CHECK; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CROSS; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.INFO; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.WARNING; + +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * Checkmarx prompts for various remediation tasks. + */ +public final class DevAssistFixPrompts { + + private DevAssistFixPrompts() { + throw new IllegalStateException("Cannot instantiate CxOneAssistFixPrompts class"); + } + + private static String getAgentName() { + return DevAssistUtils.getAgentName(); + } + + private static String getMcpDisplayName() { + return "Checkmarx"; + } + + /** + * Builds the SCA remediation prompt (generic concatenated form). + * + * @param packageName vulnerable package name (e.g. "node-ipc") + * @param packageVersion vulnerable package version (e.g. "10.1.1") + * @param packageManager ecosystem / package manager (e.g. "npm", "maven") + * @param severity textual severity (e.g. "Malicious", "High") + * @return composed prompt string (plain text with Markdown fragments) + */ + public static String buildSCARemediationPrompt(String packageName, String packageVersion, + String packageManager, String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A security issue has been detected in `").append(packageName).append("@").append(packageVersion).append("` (package manager: `").append(packageManager).append("`).\n") + .append("**Severity:** `").append(severity).append("`\n") + .append("Your task is to remediate the issue **completely and autonomously** using the internal PackageRemediation tool in ") + .append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); + + prompt.append("---\n\n") + .append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the issue type:\n") + .append("- If `status` is one of: `Critical`, `High`, `Medium`, `Low`, `Info`, set: `issueType = \"CVE\"`\n") + .append("- If `status = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") + .append("Call the internal PackageRemediation tool with:\n\n") + .append("```json\n") + .append("{\n") + .append(" \"packageName\": \"").append(packageName).append("\",\n") + .append(" \"packageVersion\": \"").append(packageVersion).append("\",\n") + .append(" \"packageManager\": \"").append(packageManager).append("\",\n") + .append(" \"issueType\": \"{determined issueType}\"\n") + .append("}\n") + .append("```\n\n") + .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the ecosystem and risk.\n") + .append("- Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n") + .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n\n"); + + prompt.append("---\n\n") + .append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `fix_instructions`, in order.\n") + .append("- For each change:\n") + .append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., dependency update, import rewrite, API refactor, test fix, TODO insertion).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n") + .append("Examples:\n") + .append("- `package.json`: lodash version changed from 3.10.1 -> 4.17.21\n") + .append("- `src/utils/date.ts`: import updated from `lodash` to `date-fns`\n") + .append("- `src/main.ts:42`: `_.pluck(users, 'id')` -> `users.map(u => u.id)`\n") + .append("- `src/index.ts:78`: // TODO: Verify API migration from old-package to new-package\n\n"); + + prompt.append("---\n\n") + .append("3. VERIFICATION:\n\n") + .append("- If the instructions include build, test, or audit steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(packageManager).append("`:\n") + .append(" - `npm`: `npx tsc --noEmit`, `npm run build`, `npm test`\n") + .append(" - `go`: `go build ./...`, `go test ./...`\n") + .append(" - `maven`: `mvn compile`, `mvn test`\n") + .append(" - `gradle`: `gradle build`, `gradle test`\n") + .append(" - `sbt`: `sbt compile`, `sbt test`\n") + .append(" - `pypi`/`setuptools`/`pyproject.toml`: `python -c \"import ").append(packageName).append("\"`, `pytest`, `python -m build`\n") + .append(" - `nuget`: `dotnet build`, `dotnet test`\n\n") + .append("If any of these validations fail:\n") + .append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n") + .append("4. OUTPUT:\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If packageRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") + .append("- **If packageRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n") + .append("Format:\n") + .append("```\n") + .append("Package: ").append(packageName).append("\n") + .append("Version: ").append(packageVersion).append("\n") + .append("Manager: ").append(packageManager).append("\n") + .append("Severity: ").append(severity).append("\n\n") + .append("Files Modified:\n") + .append("1. package.json\n") + .append(" - Updated dependency: lodash 3.10.1 → 4.17.21\n\n") + .append("2. src/utils/date.ts\n") + .append(" - Updated import: from 'lodash' to 'date-fns'\n") + .append(" - Replaced usage: _.pluck(users, 'id') → users.map(u => u.id)\n\n") + .append("3. src/__tests__/date.test.ts\n") + .append(" - Fixed test: adjusted mock expectations to match updated API\n\n") + .append("4. src/index.ts\n") + .append(" - Line 78: Inserted TODO: Verify API migration from old-package to new-package\n") + .append("```\n\n") + .append(CHECK + " **Final Status**\n\n") + .append("If all tasks succeeded:\n") + .append("- \"Remediation completed for ").append(packageName).append("@").append(packageVersion).append("\"\n") + .append("- \"All fix instructions and failing tests resolved\"\n") + .append("- \"Build status: PASS\"\n") + .append("- \"Test results: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some test failures or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n") + .append("If failed:\n") + .append("- \"Remediation failed for ").append(packageName).append("@").append(packageVersion).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or failing tests listed above\"\n\n"); + + prompt.append("---\n\n") + .append("5. CONSTRAINTS:\n\n") + .append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- Only execute what's explicitly listed in `fix_instructions`\n") + .append("- Attempt to fix test failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n"); + return prompt.toString(); + } + + + /** + * Generates a secret remediation prompt. + * + * @param title - issue title + * @param description - issue description (optional) - if null, will be empty string. + * @param severity - issue severity (optional) - if null, will be empty string. + * @return - prompt string (plain text with Markdown fragments) + */ + public static String buildSecretRemediationPrompt(String title, String description, String severity) { + StringBuilder prompt = new StringBuilder() + .append("A secret has been detected: \"").append(title).append("\" \n") + .append(description != null ? description : "").append("\n\n") + .append("---\n\n") + .append("You are the `").append(getAgentName()).append("`.\n\n") + .append("Your mission is to identify and remediate this secret using secure coding standards. Follow industry best practices, automate safely, and clearly document all actions taken.\n\n"); + + prompt.append("---\n\n") + .append("1. SEVERITY INTERPRETATION\n\n") + .append("Severity level: `").append(severity != null ? severity : "").append("`\n\n") + .append("- `Critical`: Secret is confirmed **valid**. Immediate remediation required.\n") + .append("- `High`: Secret may be valid. Treat as sensitive and externalize it securely.\n") + .append("- `Medium`: Likely **invalid** (e.g., test or placeholder). Still remove from code and annotate accordingly.\n\n"); + + prompt.append("---\n\n") + .append("2. TOOL CALL - Remediation Plan\n\n") + .append("Determine the programming language of the file where the secret was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") + .append("```json\n") + .append("{\n") + .append(" \"type\": \"secret\",\n") + .append(" \"sub_type\": \"").append(title).append("\",\n") + .append(" \"language\": \"[auto-detected language]\"\n") + .append("}\n") + .append("```\n\n") + .append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow\n") + .append(" - `best_practices` - explain secure alternatives\n") + .append(" - `description` - contextual background\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n") + .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the secret details provided\n") + .append(" - Offer practical steps and secure alternatives for secret removal\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n") + .append("3. ANALYSIS & RISK\n\n") + .append("Identify the type of secret (API key, token, credential). Explain:\n") + .append("- Why it's a risk (leakage, unauthorized access, compliance violations)\n") + .append("- What could happen if misused or left in source\n\n"); + + prompt.append("---\n\n") + .append("4. REMEDIATION STRATEGY\n\n") + .append("- Parse and apply every item in `remediation_steps` sequentially\n") + .append("- Automatically update code/config files if safe\n") + .append("- If a step cannot be applied automatically, insert a clear TODO\n") + .append("- Replace secret with environment variable or vault reference\n\n"); + + prompt.append("---\n\n") + .append("5. VERIFICATION\n\n") + .append("If applicable for the language:\n") + .append("- Run type checks or compile the code\n") + .append("- Ensure changes build and tests pass\n") + .append("- Fix issues if introduced by secret removal\n\n"); + + prompt.append("---\n\n") + .append("6. OUTPUT FORMAT\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append("Generate a structured remediation summary:\n\n") + .append("```markdown\n") + .append("### [Prefix]\n\n") + .append("**Secret:** ").append(title).append(" \n") + .append("**Severity:** ").append(severity != null ? severity : "").append(" \n") + .append("**Assessment:** ").append(getAssessmentText(severity)).append("\n\n") + .append("**Files Modified:**\n") + .append("- `.env`: Added/updated with `SECRET_NAME`\n") + .append("- `src/config.ts`: Replaced hardcoded secret with `process.env.SECRET_NAME`\n\n") + .append("**Remediation Actions Taken:**\n") + .append("- ").append(CHECK).append(" Removed hardcoded secret\n") + .append("- ").append(CHECK).append(" Inserted environment reference\n") + .append("- ").append(CHECK).append(" Updated or created .env\n") + .append("- ").append(CHECK).append(" Added TODOs for secret rotation or vault storage\n\n") + .append("**Next Steps:**\n") + .append("- [ ] Revoke exposed secret (if applicable)\n") + .append("- [ ] Store securely in vault (AWS Secrets Manager, GitHub Actions, etc.)\n") + .append("- [ ] Add CI/CD secret scanning\n\n") + .append("**Best Practices:**\n") + .append("- (From tool response, or fallback security guidelines)\n\n") + .append("**Description:**\n") + .append("- (From `description` field or fallback to original input)\n\n") + .append("```\n\n"); + + prompt.append("---\n\n") + .append("7. CONSTRAINTS\n\n") + .append("- ").append(CROSS).append(" Do NOT expose real secrets\n") + .append("- ").append(CROSS).append(" Do NOT generate fake-looking secrets\n") + .append("- ").append(CHECK).append(" Follow only what's explicitly returned from MCP\n") + .append("- ").append(CHECK).append(" Use secure externalization patterns\n") + .append("- ").append(CHECK).append(" Respect OWASP, NIST, and GitHub best practices\n"); + return prompt.toString(); + } + + /** + * Generates the assessment text for given severity. + * + * @param severity severity level + * @return assessment text + */ + private static String getAssessmentText(String severity) { + if (SeverityLevel.CRITICAL.getSeverity().equalsIgnoreCase(severity)) { + return CHECK + " Confirmed valid secret. Immediate remediation performed."; + } else if (SeverityLevel.HIGH.getSeverity().equalsIgnoreCase(severity)) { + return WARNING + " Possibly valid. Handled as sensitive."; + } else { + return INFO + " Likely invalid (test/fake). Removed for hygiene."; + } + } + + /** + * Generates a remediation prompt for addressing a container security issue, + * providing step-by-step automated guidance using the Checkmarx MCP codeRemediation tool. + * The method constructs a detailed prompt based on the identified issue. + * + * @param fileType type of the file + * @param imageName image name + * @param imageTag image tag + * @param severity severity level + * @return prompt string (plain text with Markdown fragments) + */ + public static String buildContainersRemediationPrompt(String fileType, String imageName, + String imageTag, String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A container security issue has been detected in `").append(fileType) + .append("` with image `").append(imageName).append(":").append(imageTag).append("`.\n") + .append("**Severity:** `").append(severity).append("`\n") + .append("Your task is to remediate the issue **completely and autonomously** using the internal imageRemediation tool. ") + .append("Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); + + prompt.append("---\n\n") + .append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the issue type:\n") + .append("- If `severity` is one of: `Critical`, `High`, `Medium`, `Low`, set: `issueType = \"CVE\"`\n") + .append("- If `severity = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") + .append("Call the internal imageRemediation tool with:\n\n") + .append("```json\n") + .append("{\n") + .append(" \"fileType\": \"").append(fileType).append("\",\n") + .append(" \"imageName\": \"").append(imageName).append("\",\n") + .append(" \"imageTag\": \"").append(imageTag).append("\",\n") + .append(" \"severity\": \"").append(severity).append("\"\n") + .append("}\n") + .append("```\n\n") + .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the container ecosystem and risk level.\n") + .append("- Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n") + .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the container details provided (file type, image name, image tag, severity)\n") + .append(" - Offer practical base image recommendations and step-by-step instructions for container remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n") + .append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `fix_instructions`, in order.\n") + .append("- For each change:\n") + .append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., image update, configuration change, security hardening).\n") + .append(" - Record before -> after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n") + .append("Examples:\n") + .append("- `Dockerfile`: FROM confluentinc/cp-kafkacat:6.1.10 -> FROM confluentinc/cp-kafkacat:6.2.15\n") + .append("- `docker-compose.yml`: image: vulnerable-image:1.0 -> image: secure-image:2.1\n") + .append("- `values.yaml`: repository: old-repo -> repository: new-repo\n") + .append("- `Chart.yaml`: version: 1.0.0 -> version: 1.1.0\n\n"); + + prompt.append("---\n\n") + .append("3. VERIFICATION:\n\n") + .append("- If the instructions include build, test, or deployment steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(fileType).append("`:\n") + .append(" - `Dockerfile`: `docker build .`, `docker run `\n") + .append(" - `docker-compose.yml`: `docker-compose up --build`, `docker-compose down`\n") + .append(" - `Helm Chart`: `helm lint .`, `helm template .`, `helm install --dry-run`\n\n") + .append("If any of these validations fail:\n") + .append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n") + .append("4. OUTPUT:\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If imageRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") + .append("- **If imageRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n") + .append("Format:\n") + .append("```\n") + .append("File Type: ").append(fileType).append("\n") + .append("Image: ").append(imageName).append(":").append(imageTag).append("\n") + .append("Severity: ").append(severity).append("\n\n") + .append("Files Modified:\n") + .append("1. ").append(fileType).append("\n") + .append(" - Updated image: ").append(imageName).append(":").append(imageTag).append(" → secure version\n\n") + .append("2. docker-compose.yml (if applicable)\n") + .append(" - Updated service configuration to use secure image\n\n") + .append("3. values.yaml (if applicable)\n") + .append(" - Updated Helm chart values for secure deployment\n\n") + .append("4. README.md\n") + .append(" - Updated documentation with new image version\n") + .append("```\n\n") + .append(CHECK + " **Final Status**\n\n") + .append("If all tasks succeeded:\n") + .append("- \"Remediation completed for ").append(imageName).append(":").append(imageTag).append("\"\n") + .append("- \"All fix instructions and deployment tests resolved\"\n") + .append("- \"Build status: PASS\"\n") + .append("- \"Deployment status: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some deployment steps or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n") + .append("If failed:\n") + .append("- \"Remediation failed for ").append(imageName).append(":").append(imageTag).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or deployment issues listed above\"\n\n"); + + prompt.append("---\n\n") + .append("5. CONSTRAINTS:\n\n") + .append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- Only execute what's explicitly listed in `fix_instructions`\n") + .append("- Attempt to fix deployment failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow container security best practices (non-root user, minimal base images, etc.)\n"); + return prompt.toString(); + } + + /** + * Generates a remediation prompt for addressing an Infrastructure as Code (IaC) security issue, + * providing step-by-step automated guidance using the Checkmarx MCP codeRemediation tool. + * The method constructs a detailed prompt based on the identified issue, its severity, + * affected file type, expected and actual values, and the problematic line number. + * + * @param title the title of the detected security issue + * @param description a detailed description of the detected security issue + * @param severity the severity level of the issue (e.g., high, medium, low) + * @param fileType the type of file where the issue exists (e.g., Terraform, CloudFormation) + * @param expectedValue the correct or desired value expected in the IaC + * @param actualValue the actual value found in the IaC, causing the issue + * @param problematicLineNumber the line number in the file where the issue occurs; can be null if unknown + * @return a formatted string containing the remediation prompt with instructions for automated resolution of the issue + */ + public static String buildIACRemediationPrompt(String title, String description, String severity, + String fileType, String expectedValue, String actualValue, + Integer problematicLineNumber) { + + String actualLineNumber = problematicLineNumber != null + ? String.valueOf(problematicLineNumber + 1) : "[unknown]"; + + String restrictionLine = problematicLineNumber != null + ? String.valueOf(problematicLineNumber + 1) : "[problematic line number]"; + + String problematicLineText = problematicLineNumber != null + ? "**Problematic Line Number:** " + (problematicLineNumber + 1) : ""; + + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n"); + prompt.append("An Infrastructure as Code (IaC) security issue has been detected.\n\n") + .append("**Issue:** `").append(title).append("`\n") + .append("**Severity:** `").append(severity).append("`\n") + .append("**File Type:** `").append(fileType).append("`\n") + .append("**Description:** ").append(description).append("\n") + .append("**Expected Value:** ").append(expectedValue).append("\n") + .append("**Actual Value:** ").append(actualValue).append("\n") + .append(problematicLineText).append("\n\n"); + + prompt.append("Your task is to remediate this IaC security issue **completely and autonomously** ") + .append("using the internal codeRemediation tool in ").append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n"); + prompt.append(WARNING).append("️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") + .append(actualLineNumber) + .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); + + prompt.append("---\n\n") + .append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the programming language of the file where the IaC security issue was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") + .append("```json\n") + .append("{\n") + .append(" \"language\": \"[auto-detected programming language]\",\n") + .append(" \"metadata\": {\n") + .append(" \"title\": \"").append(title).append("\",\n") + .append(" \"description\": \"").append(description).append("\",\n") + .append(" \"remediationAdvice\": \"").append(expectedValue).append("\"\n") + .append(" },\n") + .append(" \"sub_type\": \"\",\n") + .append(" \"type\": \"iac\"\n") + .append("}\n") + .append("```\n\n") + .append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow for remediation\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n") + .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the IaC details provided (title, description, expected vs. actual values)\n") + .append(" - Offer practical configuration examples and step-by-step instructions for remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n") + .append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `remediation_steps`, in order.\n") + .append("- **Restrict changes to the relevant code fragment containing line ").append(restrictionLine).append("**.\n") + .append("- For each change:\n") + .append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., configuration update, security hardening, permission changes, encryption settings).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n"); + + prompt.append("---\n\n") + .append("3. VERIFICATION:\n\n") + .append("- If the instructions include validation, deployment, or testing steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(fileType).append("`:\n") + .append(" - `Terraform`: `terraform validate`, `terraform plan`\n") + .append(" - `CloudFormation`: `aws cloudformation validate-template`\n") + .append(" - `Kubernetes`: `kubectl apply --dry-run=client`\n") + .append(" - `Docker`: `docker-compose config`\n\n") + .append("If any of these validations fail:\n") + .append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n") + .append("4. OUTPUT:\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n") + .append("Format:\n") + .append("```\n") + .append("Issue: ").append(title).append("\n") + .append("Severity: ").append(severity).append("\n") + .append("File Type: ").append(fileType).append("\n") + .append("Problematic Line: ").append(actualLineNumber).append("\n\n") + .append("Files Modified:\n") + .append("1. ").append(fileType).append("\n") + .append(" - Updated configuration: ").append(actualValue).append(" → ").append(expectedValue).append("\n") + .append(" - Applied security hardening based on best practices\n\n") + .append("2. Additional configurations (if applicable)\n") + .append(" - Updated related security settings\n") + .append(" - Added missing security controls\n\n") + .append("3. Documentation\n") + .append(" - Updated comments and documentation where applicable\n") + .append("```\n\n") + .append(CHECK + " **Final Status**\n\n") + .append("If all tasks succeeded:\n") + .append("- \"Remediation completed for IaC security issue ").append(title).append("\"\n") + .append("- \"All fix instructions and security validations resolved\"\n") + .append("- \"Configuration validation: PASS\"\n") + .append("- \"Security compliance: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some security validations or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n") + .append("If failed:\n") + .append("- \"Remediation failed for IaC security issue ").append(title).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or security issues listed above\"\n\n"); + + prompt.append("---\n\n") + .append("5. CONSTRAINTS:\n\n") + .append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- **Only modify the code that corresponds to the identified problematic line**\n") + .append("- Attempt to fix validation failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow Infrastructure as Code security best practices throughout the process\n"); + return prompt.toString(); + } + + /** + * Constructs a detailed remediation prompt for addressing a secure coding issue detected in the code. + * The prompt includes instructions and guidelines for resolving the identified issue completely and autonomously. + * + * @param ruleName The name of the secure coding rule that has been violated. + * @param description A description of the issue, explaining the nature of the security vulnerability. + * @param severity The severity level of the detected issue (e.g., low, medium, high, critical). + * @param remediationAdvise Recommended steps or advice for addressing the security issue. + * @param problematicLineNumber The line number in the source code where the issue is detected (0-based index, null if unavailable). + * @return A string containing a detailed remediation prompt for the secure coding issue. + */ + public static String buildASCARemediationPrompt(String ruleName, String description, + String severity, String remediationAdvise, Integer problematicLineNumber) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A secure coding issue has been detected in your code.\n\n") + .append("**Rule:** `").append(ruleName).append("` \n") + .append("**Severity:** `").append(severity).append("` \n") + .append("**Description:** ").append(description).append(" \n") + .append("**Recommended Fix:** ").append(remediationAdvise).append(" \n"); + + if (problematicLineNumber != null) { + prompt.append("**Problematic Line Number:** ").append(problematicLineNumber + 1).append("\n\n"); + } else { + prompt.append("\n"); + } + + prompt.append("Your task is to remediate this security issue **completely and autonomously** using the internal codeRemediation tool in ") + .append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n") + .append(WARNING).append("️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[problematic line number]") + .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); + + prompt.append("---\n\n") + .append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the programming language of the file where the security issue was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") + .append("```json\n") + .append("{\n") + .append(" \"language\": \"[auto-detected programming language]\",\n") + .append(" \"metadata\": {\n") + .append(" \"ruleID\": \"").append(ruleName).append("\",\n") + .append(" \"description\": \"").append(description).append("\",\n") + .append(" \"remediationAdvice\": \"").append(remediationAdvise).append("\"\n") + .append(" },\n") + .append(" \"sub_type\": \"\",\n") + .append(" \"type\": \"sast\"\n") + .append("}\n") + .append("```\n\n") + .append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow for remediation\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n") + .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the issue details provided (rule name, description, severity, and recommended fix)\n") + .append(" - Offer practical code examples and step-by-step instructions for manual remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n") + .append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `remediation_steps`, in order.\n") + .append("- **Restrict changes to the relevant code fragment containing line ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]") + .append("**.\n") + .append("- For each change:\n") + .append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., input validation, sanitization, secure API usage, authentication fix).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n"); + + prompt.append("---\n\n") + .append("3. OUTPUT:\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n") + .append("Format:\n") + .append("```\n") + .append("Rule: ").append(ruleName).append("\n") + .append("Severity: ").append(severity).append("\n") + .append("Issue Type: SAST Security Vulnerability\n") + .append("Problematic Line: ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]").append("\n\n") + .append("Files Modified:\n") + .append("1. src/auth.ts\n") + .append(" - Line 42: Replaced plain text comparison with bcrypt.compare()\n") + .append(" - Added secure password hashing implementation\n\n") + .append("2. src/db.ts\n") + .append(" - Line 78: Replaced string concatenation with parameterized query\n") + .append(" - Prevented SQL injection vulnerability\n\n") + .append("3. src/api.ts\n") + .append(" - Line 156: Added input validation for email parameter\n") + .append(" - Implemented sanitization for user inputs\n\n") + .append("4. src/config.ts\n") + .append(" - Line 23: Inserted TODO for production security review\n") + .append("```\n\n") + .append(CHECK + " **Final Status**\n\n") + .append("If all tasks succeeded:\n") + .append("- \"Remediation completed for security rule ").append(ruleName).append("\"\n") + .append("- \"All fix instructions and security validations resolved\"\n") + .append("- \"Build status: PASS\"\n") + .append("- \"Security tests: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some security validations or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n") + .append("If failed:\n") + .append("- \"Remediation failed for security rule ").append(ruleName).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or security issues listed above\"\n\n"); + + prompt.append("---\n\n") + .append("4. CONSTRAINTS:\n\n") + .append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- **Only modify the code that corresponds to the identified problematic line**\n") + .append("- Attempt to fix build/test failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow secure coding best practices throughout the process\n"); + return prompt.toString(); + } + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/NotificationPopup.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/NotificationPopup.java new file mode 100644 index 00000000..ddafdca6 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/NotificationPopup.java @@ -0,0 +1,36 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; + +/** + * Generic class to display notification pop-up (balloon) in Eclipse. This class + * is used to display a message with a title in a pop-up window. + */ +public class NotificationPopup extends AbstractNotificationPopup { + + private final String message; + private final String title; + + public NotificationPopup(Display display, String title, String message) { + super(display); + this.title = title; + this.message = message; + } + + @Override + protected void createContentArea(Composite parent) { + Label label = new Label(parent, SWT.WRAP); + label.setText(message); + label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + } + + @Override + protected String getPopupShellTitle() { + return title; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java new file mode 100644 index 00000000..ad908997 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java @@ -0,0 +1,392 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.QUICK_FIX; +import static java.lang.String.format; + +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.swt.widgets.Display; +import org.slf4j.Logger; + +import com.checkmarx.eclipse.common.utils.CxLogger; +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; + +/** + * RemediationManager provides remediation options for issues identified during + * a real-time scan. + *

+ * This class supports applying fixes, viewing details etc. for scan issues + * detected by different scan engines, such as OSS, ASCA, etc. + *

+ * Main responsibilities: + *

    + *
  • Apply remediation for different scan engine issues
  • + *
  • Generate and copy remediation prompts to the clipboard
  • + *
  • Log remediation actions
  • + *
+ */ +public final class RemediationManager { + +// private static final Logger LOGGER = PluginUtils.getLogger(RemediationManager.class); + + private static final String DEV_ASSIST_COPY_FIX_PROMPT = "Fix prompt copied to clipboard! Paste the prompt into Copilot chat (Agent Mode)"; + + private static final String DEV_ASSIST_COPY_VIEW_DETAILS_PROMPT = "Prompt asking AI to provide more details was copied to your clipboard! Paste the prompt into Copilot chat."; + + /** + * Apply remediation for a given scan issue. + * + * @param project the project where the fix is to be applied + * @param scanIssue the scan issue to fix + * @param actionId the action ID for vulnerability-specific fixes + */ + public void fixWithCxOneAssist(@NonNull ScanIssue scanIssue, String actionId) { + String prompt = buildRemediationPrompt(scanIssue, actionId); + applyFix(scanIssue, prompt); + } + + /** + * Builds the remediation prompt based on scan engine type. + * + * @param scanIssue the scan issue to build prompt for + * @param actionId the action ID for vulnerability-specific fixes + * @return the remediation prompt, or null if not applicable + */ + @Nullable + private String buildRemediationPrompt(@NonNull ScanIssue scanIssue, String actionId) { + switch (scanIssue.getScanEngine()) { + case OSS: + return buildOSSRemediationPrompt(scanIssue); + case SECRETS: + return buildSecretRemediationPrompt(scanIssue); + case CONTAINERS: + return buildContainerRemediationPrompt(scanIssue); + case IAC: + return buildIACRemediationPrompt(scanIssue, actionId); + case ASCA: + return buildASCARemediationPrompt(scanIssue, actionId); + default: + return null; + } + } + + /** + * Applies the fix by attempting to send to Copilot AI first, with clipboard + * fallback. + * + * @param project the project context + * @param scanIssue the scan issue being fixed + * @param prompt the remediation prompt to apply + */ + private void applyFix(@NonNull ScanIssue scanIssue, @Nullable String prompt) { + if (prompt == null || prompt.isEmpty()) { + CxLogger.warning(format("RTS-Fix: Remediation failed. Prompt is empty for issue: %s, for file: %s", + scanIssue.getTitle(), scanIssue.getFilePath())); + return; + } + CxLogger.info(format("RTS-Fix: %s remediation started for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + String notificationTitle = getNotificationTitle(scanIssue.getScanEngine()); + + // Try to fix with Copilot AI first (no notifications shown by fixWithAI) + boolean aiSuccess = fixWithAI(prompt); + if (aiSuccess) { + CxLogger.info(format("RTS-Fix: %s remediation sent to Copilot for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + } else { + // Fallback: Copy to clipboard with notification when Copilot is not available + if (copyToClipboardAndNotify(prompt,notificationTitle, DEV_ASSIST_COPY_FIX_PROMPT)) { + CxLogger.info(format("RTS-Fix: %s remediation completed (clipboard) for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + } + } + } + + /** + * Sends a fix prompt to GitHub Copilot for automated remediation. + *

+ * This method attempts to: + *

    + *
  1. Open GitHub Copilot Chat
  2. + *
  3. Switch to Agent mode
  4. + *
  5. Paste and send the prompt automatically
  6. + *
+ *

+ * This method does NOT show any notifications - the caller is responsible for + * handling success/failure notifications. + * + * @param prompt the fix prompt to send to Copilot + * @param project the project context + * @return true if Copilot was successfully opened and prompt initiated, false + * otherwise + */ + private boolean fixWithAI(@NonNull String prompt) { + try { + return CopilotIntegration.sendPromptToCopilot(prompt); + } catch (Exception exception) { + CxLogger.error("RTS-Fix: Failed to fix with AI: ", exception); + return false; + } + } + + /** + * View details for a given scan issue. + * + * @param project the project where the fix is to be applied + * @param scanIssue the scan issue to view details for + * @param actionId the action ID for vulnerability-specific details + */ + public void viewDetails(@NonNull ScanIssue scanIssue, String actionId) { + String prompt = buildExplanationPrompt(scanIssue, actionId); + applyViewDetails(scanIssue, prompt); + } + + /** + * Builds the explanation prompt based on scan engine type. + * + * @param scanIssue the scan issue to build prompt for + * @param actionId the action ID for vulnerability-specific details + * @return the explanation prompt, or null if not applicable + */ + @Nullable + private String buildExplanationPrompt(@NonNull ScanIssue scanIssue, String actionId) { + switch (scanIssue.getScanEngine()) { + case OSS: + return buildOSSExplanationPrompt(scanIssue); + case SECRETS: + return buildSecretExplanationPrompt(scanIssue); + case CONTAINERS: + return buildContainerExplanationPrompt(scanIssue); + case IAC: + return buildIACExplanationPrompt(scanIssue, actionId); + case ASCA: + return buildASCAExplanationPrompt(scanIssue, actionId); + default: + return null; + } + } + + /** + * Applies the view details by attempting to send to Copilot AI first, with + * clipboard fallback. + * + * @param project the project context + * @param scanIssue the scan issue being explained + * @param prompt the explanation prompt to apply + */ + private void applyViewDetails(@NonNull ScanIssue scanIssue, @Nullable String prompt) { + if (prompt == null || prompt.isEmpty()) { + CxLogger.warning(format("RTS-ViewDetails: Explanation failed. Prompt is empty for issue: %s, for file: %s", + scanIssue.getTitle(), scanIssue.getFilePath())); + return; + } + CxLogger.info(format("RTS-ViewDetails: %s explanation started for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + String notificationTitle = getNotificationTitle(scanIssue.getScanEngine()); + + // Try to send to Copilot AI first (no notifications shown by fixWithAI) + boolean aiSuccess = fixWithAI(prompt); + if (aiSuccess) { + CxLogger.info(format("RTS-ViewDetails: %s explanation sent to Copilot for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + } else { + // Fallback: Copy to clipboard with notification when Copilot is not available + if (copyToClipboardAndNotify(prompt, notificationTitle, DEV_ASSIST_COPY_VIEW_DETAILS_PROMPT)) { + CxLogger.info(format("RTS-ViewDetails: %s explanation completed (clipboard) for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + } + } + } + + /** + * Builds remediation prompt for an OSS issue. + */ + private String buildOSSRemediationPrompt(ScanIssue scanIssue) { + return DevAssistFixPrompts.buildSCARemediationPrompt(scanIssue.getTitle(), scanIssue.getPackageVersion(), + scanIssue.getPackageManager(), scanIssue.getSeverity()); + } + + /** + * Builds remediation prompt for a Secret issue. + */ + private String buildSecretRemediationPrompt(ScanIssue scanIssue) { + return DevAssistFixPrompts.buildSecretRemediationPrompt(scanIssue.getTitle(), scanIssue.getDescription(), + scanIssue.getSeverity()); + } + + /** + * Builds remediation prompt for a container issue. + */ + private String buildContainerRemediationPrompt(ScanIssue scanIssue) { + return DevAssistFixPrompts.buildContainersRemediationPrompt(scanIssue.getFileType(), scanIssue.getTitle(), + scanIssue.getImageTag(), scanIssue.getSeverity()); + } + + /** + * Builds remediation prompt for a IAC issue. + */ + private String buildIACRemediationPrompt(ScanIssue scanIssue, String actionId) { + if (Objects.isNull(actionId) || actionId.isEmpty()) { + CxLogger.warning(format("RTS-Fix: Remediation failed. Action id is not found for IAC issue: %s.", + scanIssue.getTitle())); + return null; + } + Vulnerability vulnerability = DevAssistUtils.getVulnerabilityDetails(scanIssue, + actionId.equals(QUICK_FIX) ? scanIssue.getScanIssueId() : actionId); + + if (Objects.isNull(vulnerability)) { + CxLogger.warning(format("RTS-Fix: Remediation failed. Vulnerability details not found for IAC issue: %s.", + actionId)); + return null; + } + + return DevAssistFixPrompts.buildIACRemediationPrompt( + actionId.equals(QUICK_FIX) ? scanIssue.getTitle() : vulnerability.getTitle(), + actionId.equals(QUICK_FIX) ? scanIssue.getDescription() : vulnerability.getDescription(), + actionId.equals(QUICK_FIX) ? scanIssue.getSeverity() : vulnerability.getSeverity(), + scanIssue.getFileType(), vulnerability.getExpectedValue(), vulnerability.getActualValue(), + scanIssue.getProblematicLineNumber()); + } + + /** + * Builds remediation prompt for an ASCA issue. + * + * @param scanIssue the scan issue to fix + * @param actionId the specific vulnerability ID to fix, or QUICK_FIX for + * general remediation + */ + private String buildASCARemediationPrompt(ScanIssue scanIssue, String actionId) { + if (Objects.isNull(actionId) || actionId.isEmpty()) { + CxLogger.warning(format("RTS-Fix: Remediation failed. Action id is not found for ASCA issue: %s.", + scanIssue.getTitle())); + return null; + } + Vulnerability vulnerability = DevAssistUtils.getVulnerabilityDetails(scanIssue, + actionId.equals(QUICK_FIX) ? scanIssue.getScanIssueId() : actionId); + + if (Objects.isNull(vulnerability)) { + CxLogger.warning(format("RTS-Fix: Remediation failed. Vulnerability details not found for ASCA issue: %s.", + actionId)); + return null; + } + + return DevAssistFixPrompts.buildASCARemediationPrompt( + actionId.equals(QUICK_FIX) ? scanIssue.getTitle() : vulnerability.getTitle(), + actionId.equals(QUICK_FIX) ? scanIssue.getDescription() : vulnerability.getDescription(), + actionId.equals(QUICK_FIX) ? scanIssue.getSeverity() : vulnerability.getSeverity(), + actionId.equals(QUICK_FIX) ? scanIssue.getRemediationAdvise() : vulnerability.getRemediationAdvise(), + scanIssue.getProblematicLineNumber()); + } + + /** + * Builds explanation prompt for an OSS issue. + */ + private String buildOSSExplanationPrompt(ScanIssue scanIssue) { + return ViewDetailsPrompts.buildSCAExplanationPrompt(scanIssue.getTitle(), scanIssue.getPackageVersion(), + scanIssue.getSeverity(), scanIssue.getVulnerabilities()); + } + + /** + * Builds explanation prompt for a Secret issue. + */ + private String buildSecretExplanationPrompt(ScanIssue scanIssue) { + return ViewDetailsPrompts.buildSecretsExplanationPrompt(scanIssue.getTitle(), scanIssue.getDescription(), + scanIssue.getSeverity()); + } + + /** + * Builds explanation prompt for a container issue. + */ + private String buildContainerExplanationPrompt(ScanIssue scanIssue) { + return ViewDetailsPrompts.buildContainersExplanationPrompt(scanIssue.getFileType(), scanIssue.getTitle(), + scanIssue.getImageTag(), scanIssue.getSeverity()); + } + + /** + * Builds explanation prompt for an IAC issue. + */ + private String buildIACExplanationPrompt(ScanIssue scanIssue, String actionId) { + if (Objects.isNull(actionId) || actionId.isEmpty()) { + CxLogger.warning(format("RTS-ViewDetails: Explanation failed. Action id is not found for IAC issue: %s.", + scanIssue.getTitle())); + return null; + } + Vulnerability vulnerability = DevAssistUtils.getVulnerabilityDetails(scanIssue, + actionId.equals(QUICK_FIX) ? scanIssue.getScanIssueId() : actionId); + + if (Objects.isNull(vulnerability)) { + CxLogger.warning( + format("RTS-ViewDetails: Explanation failed. Vulnerability details not found for IAC issue: %s.", + actionId)); + return null; + } + + return ViewDetailsPrompts.buildIACExplanationPrompt( + actionId.equals(QUICK_FIX) ? scanIssue.getTitle() : vulnerability.getTitle(), + actionId.equals(QUICK_FIX) ? scanIssue.getDescription() : vulnerability.getDescription(), + actionId.equals(QUICK_FIX) ? scanIssue.getSeverity() : vulnerability.getSeverity(), + scanIssue.getFileType(), vulnerability.getExpectedValue(), vulnerability.getActualValue()); + } + + /** + * Builds explanation prompt for an ASCA issue. + * + * @param scanIssue the scan issue to explain + * @param actionId the specific vulnerability ID to explain, or QUICK_FIX for + * general explanation + */ + private String buildASCAExplanationPrompt(ScanIssue scanIssue, String actionId) { + if (Objects.isNull(actionId) || actionId.isEmpty()) { + CxLogger.warning(format("RTS-ViewDetails: Explanation failed. Action id is not found for ASCA issue: %s.", + scanIssue.getTitle())); + return null; + } + Vulnerability vulnerability = DevAssistUtils.getVulnerabilityDetails(scanIssue, + actionId.equals(QUICK_FIX) ? scanIssue.getScanIssueId() : actionId); + + if (Objects.isNull(vulnerability)) { + CxLogger.warning( + format("RTS-ViewDetails: Explanation failed. Vulnerability details not found for ASCA issue: %s.", + actionId)); + return null; + } + + return ViewDetailsPrompts.buildASCAExplanationPrompt( + actionId.equals(QUICK_FIX) ? scanIssue.getTitle() : vulnerability.getTitle(), + actionId.equals(QUICK_FIX) ? scanIssue.getDescription() : vulnerability.getDescription(), + actionId.equals(QUICK_FIX) ? scanIssue.getSeverity() : vulnerability.getSeverity()); + } + + /** + * Get the notification title for the given scan engine. + */ + private String getNotificationTitle(ScanEngine scanEngine) { + return DevAssistUtils.getAgentName() + " - " + scanEngine.name(); + } + + /** + * Copies the prompt to the clipboard and shows a balloon notification + * confirming it. + * + * @param prompt the prompt to copy + * @return true if the prompt was successfully copied + */ + private static boolean copyToClipboardAndNotify(String prompt, String notifyTitle, String notifyMessage) { + boolean copied = DevAssistUtils.copyToClipboard(prompt); + if (copied) { + Display.getDefault().asyncExec(() -> { + Display display = Display.getDefault(); + NotificationPopup popup = new NotificationPopup(display, notifyTitle, notifyMessage); + popup.open(); + }); + } else { + CxLogger.error("RTS-Fix: Failed to copy prompt to clipboard", new Exception("RTS-Fix: Failed to copy prompt to clipboard")); + } + return copied; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/ViewDetailsPrompts.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/ViewDetailsPrompts.java new file mode 100644 index 00000000..6edf60d7 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/ViewDetailsPrompts.java @@ -0,0 +1,439 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.BOOKS; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.BRAIN; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CHECK; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CLIPBOARD; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CONSTRUCTION; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.CROSS; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.EXCLAMATION; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.FIRECRACKER; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.LOCK; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.OPEN_BOOK; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.PENCIL; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.POINT_RIGHT; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.POLICE_LIGHT; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.REPEAT; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.SEARCH; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.SHIELD; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.TOOLS; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.WARNING; +import static com.checkmarx.eclipse.devassist.utils.EmojiUnicodes.WHALE; + +import java.util.List; + +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * Prompt builder for generating prompts for viewing vulnerability details. + */ +public final class ViewDetailsPrompts { + + private ViewDetailsPrompts() { + throw new IllegalStateException("Cannot instantiate ViewDetailsPrompts class"); + } + + private static final String AGENT_NAME = DevAssistUtils.getAgentName(); + + /** + * Generates a detailed prompt for explaining the security issue of an SCA/OSS. + * + * @param packageName The name of the software package. + * @param version The version of the software package. + * @param status The security status of the package (e.g., "malicious", "vulnerable"). + * @param vulnerabilities A list of vulnerabilities affecting the package. + * @return A formatted prompt string for explaining the security issue. + */ + public static String buildSCAExplanationPrompt(String packageName, String version, + String status, List vulnerabilities) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the `").append(AGENT_NAME).append("`.\n\n") + .append("Your task is to **analyze and explain** the security issue affecting the package `") + .append(packageName).append("@").append(version).append("` with status: `").append(status).append("`.\n\n"); + + prompt.append("### " + EXCLAMATION + " Important Instructions:\n") + .append("- " + POINT_RIGHT + " **Do not change anything in the code. Just explain the risks and remediation steps.**\n") + .append("- " + CROSS + " **Never include references from Checkmarx competitors.**\n\n"); + + prompt.append("### " + SEARCH + " Package Overview\n\n") + .append("- **Package:** `").append(packageName).append("`\n") + .append("- **Version:** `").append(version).append("`\n") + .append("- **Status:** `").append(status).append("`\n"); + + if (status.equalsIgnoreCase(SeverityLevel.MALICIOUS.getSeverity())) { + buildMaliciousContentForSCAPrompt(version, prompt); + } else { + buildVulnerabilityContentForSCAPrompt(status, vulnerabilities, prompt); + } + prompt.append("### " + TOOLS + " Remediation Guidance\n\n") + .append("Offer actionable advice:\n") + .append("- Whether to remove, upgrade, or replace the package\n") + .append("- If malicious: clearly emphasize permanent removal\n") + .append("- Recommend safer, verified alternatives if available\n") + .append("- Suggest preventative measures:\n") + .append(" - Use SCA in CI/CD\n") + .append(" - Prefer signed packages\n") + .append(" - Pin versions to prevent shadow updates\n\n"); + + prompt.append("### " + CHECK + " Summary Section\n\n") + .append("Conclude with:\n") + .append("- Overall risk explanation\n") + .append("- Immediate remediation steps\n") + .append("- Whether this specific version is linked to online reports\n") + .append("- If not, reference Checkmarx attribution (per above rules)\n") + .append("- Never mention competitor vendors or tools\n\n"); + + prompt.append("### " + PENCIL + " Output Formatting\n\n") + .append("- Use Markdown: `##`, `- `, `**bold**`, `code`\n") + .append("- Developer-friendly tone, informative, concise\n") + .append("- No speculation - use only trusted, verified sources\n"); + + return prompt.toString(); + } + + /** + * Builds a prompt for explaining malicious packages. + * + * @param version the version of the package + * @param prompt the prompt builder + */ + private static void buildMaliciousContentForSCAPrompt(String version, StringBuilder prompt) { + prompt.append("### " + FIRECRACKER + " Malicious Package Detected\n\n") + .append("This package has been flagged as **malicious**.\n\n") + .append("** " + WARNING + " Never install or use this package under any circumstances.**\n\n") + .append("#### " + SEARCH + " Web Investigation:\n\n") + .append("- Search the web for trusted community or vendor reports about malicious activity involving this package.\n") + .append("- If information exists about other versions but **not** version `").append(version).append("`, explicitly say:\n\n") + .append("> _“This specific version (`").append(version).append("`) was identified as malicious by Checkmarx Security researchers.”_\n\n") + .append("- If **no credible external information is found at all**, state:\n\n") + .append("> _“This package was identified as malicious by Checkmarx Security researchers based on internal threat intelligence and behavioral analysis.”_\n\n") + .append("Then explain:\n") + .append("- What types of malicious behavior these packages typically include (e.g., data exfiltration, postinstall backdoors)\n") + .append("- Indicators of compromise developers should look for (e.g., suspicious scripts, obfuscation, DNS calls)\n\n") + .append("**Recommended Actions:**\n") + .append("- " + CHECK + " Immediately remove from all codebases and pipelines\n") + .append("- " + CROSS + " Never reinstall or trust any version of this package\n") + .append("- " + REPEAT + " Replace with a well-known, secure alternative\n") + .append("- " + LOCK + " Consider running a retrospective security scan if this was installed\n\n"); + } + + /** + * Builds a prompt for explaining known vulnerabilities. + * + * @param status the severity status of the package + * @param vulnerabilities the list of vulnerabilities affecting the package + * @param prompt the prompt builder + */ + private static void buildVulnerabilityContentForSCAPrompt(String status, List vulnerabilities, StringBuilder prompt) { + prompt.append("### " + POLICE_LIGHT + " Known Vulnerabilities\n\n") + .append("Explain each known CVE affecting this package:\n"); + + if (vulnerabilities != null && !vulnerabilities.isEmpty()) { + for (int i = 0; i < vulnerabilities.size(); i++) { + Vulnerability vuln = vulnerabilities.get(i); + prompt.append("\n#### ").append(i + 1).append(". ").append(vuln.getCve()).append("\n") + .append("- **Severity:** ").append(vuln.getSeverity()).append("\n") + .append("- **Description:** ").append(vuln.getDescription()).append("\n"); + } + prompt.append("\n"); + } else { + prompt.append("\n " + WARNING + " No CVEs were provided. Please verify if this is expected for status `").append(status).append("`.\n\n"); + } + } + + /** + * Generates a detailed prompt for explaining a detected secret. + * + * @param title the title of the secret + * @param description the description of the secret + * @param severity the severity level of the secret vulnerability + * @return the formatted Markdown prompt + */ + public static String buildSecretsExplanationPrompt(String title, String description, String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the `").append(AGENT_NAME).append("`.\n\n") + .append("A potential secret has been detected: **\"").append(title).append("\"** \n") + .append("Severity: **").append(severity).append("**\n\n"); + prompt.append("### " + EXCLAMATION + " Important Instruction:\n") + .append(POINT_RIGHT + " **Do not change any code. Just explain the risk, validation level, and recommended actions.**\n\n"); + prompt.append("### " + SEARCH + " Secret Overview\n\n") + .append("- **Secret Name:** `").append(title).append("`\n") + .append("- **Severity Level:** `").append(severity).append("`\n") + .append("- **Details:** ").append(description).append("\n\n"); + prompt.append("### " + BRAIN + " Risk Understanding Based on Severity\n\n") + .append("- **Critical**: \n") + .append(" The secret was **validated as active**. It is likely in use and can be exploited immediately if exposed.\n\n") + .append("- **High**: \n") + .append(" The validation status is **unknown**. The secret may or may not be valid. Proceed with caution and treat it as potentially live.\n\n") + .append("- **Medium**: \n") + .append(" The secret was identified as **invalid** or **mock/test value**. While not active, it may confuse developers or be reused insecurely.\n\n"); + prompt.append("### " + LOCK + " Why This Matters\n\n") + .append("Hardcoded secrets pose a serious risk:\n") + .append("- **Leakage** through public repositories or logs\n") + .append("- **Unauthorized access** to APIs, cloud providers, or infrastructure\n") + .append("- **Exploitation** via replay attacks, privilege escalation, or lateral movement\n\n"); + prompt.append("### " + CHECK + " Recommended Remediation Steps (for developer action)\n\n") + .append("- Rotate the secret if it's live (Critical/High)\n") + .append("- Move secrets to environment variables or secret managers\n") + .append("- Audit the commit history to ensure it hasn't leaked publicly\n") + .append("- Implement secret scanning in your CI/CD pipelines\n") + .append("- Document safe handling procedures in your repo\n\n"); + prompt.append("### " + CLIPBOARD + " Next Steps Checklist (Markdown)\n\n") + .append("```markdown\n") + .append("### Next Steps:\n") + .append("- [ ] Rotate the exposed secret if valid\n") + .append("- [ ] Move secret to secure storage (.env or secret manager)\n") + .append("- [ ] Clean secret from commit history if leaked\n") + .append("- [ ] Annotate clearly if it's a fake or mock value\n") + .append("- [ ] Implement CI/CD secret scanning and policies\n") + .append("```\n\n"); + prompt.append("### " + PENCIL + " Output Format Guidelines\n\n") + .append("- Use Markdown with clear sections\n") + .append("- Do not attempt to edit or redact the code\n") + .append("- Be factual, concise, and helpful\n") + .append("- Assume this is shown to a developer unfamiliar with security tooling\n"); + return prompt.toString(); + } + + /** + * Generates a detailed prompt for explaining a detected container issue. + * + * @param fileType the file type of the container vulnerability + * @param imageName the name of the image + * @param imageTag the tag of the image + * @param severity the severity level of the container vulnerability + * @return the formatted Markdown prompt + */ + public static String buildContainersExplanationPrompt(String fileType, String imageName, + String imageTag, String severity) { + boolean isMalicious = severity.equalsIgnoreCase(SeverityLevel.MALICIOUS.getSeverity()); + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the `").append(AGENT_NAME).append("`.\n\n") + .append("Your task is to **analyze and explain** the container security issue affecting `") + .append(fileType).append("` with image `").append(imageName).append(":").append(imageTag) + .append("` and severity: `").append(severity).append("`.\n\n"); + prompt.append("### Important Instructions:\n") + .append("- **Do not change anything in the code. Just explain the risks and remediation steps.**\n") + .append("- **Never include references from Checkmarx competitors.**\n\n"); + prompt.append("### " + SEARCH + " Container Overview\n\n") + .append("- **File Type:** `").append(fileType).append("`\n") + .append("- **Image:** `").append(imageName).append(":").append(imageTag).append("`\n") + .append("- **Severity:** `").append(severity).append("`\n\n"); + prompt.append("### " + WHALE + " Container Security Issue Analysis\n\n") + .append("**Issue Type:** ") + .append(isMalicious ? "Malicious Container Image" : "Vulnerable Container Image") + .append("\n\n"); + + if (isMalicious) { + // Malicious content + buildMaliciousContentForContainerPrompt(prompt, imageTag); + } else { + // Vulnerable content + buildVulnerabilityContentForContainerPrompt(prompt); + } + prompt.append("### " + TOOLS + " Remediation Guidance\n\n") + .append("Offer actionable advice:\n") + .append("- Whether to update, replace, or rebuild the container\n") + .append("- If malicious: clearly emphasize permanent removal\n") + .append("- Recommend secure base images and best practices\n") + .append("- Suggest preventative measures:\n") + .append(" - Use container scanning in CI/CD\n") + .append(" - Prefer minimal base images (Alpine, distroless)\n") + .append(" - Implement image signing and verification\n") + .append(" - Regular security updates and patching\n") + .append(" - Run containers as non-root users\n") + .append(" - Use multi-stage builds to reduce attack surface\n\n"); + prompt.append("### " + CHECK + " Summary Section\n\n") + .append("Conclude with:\n") + .append("- Overall risk explanation for container deployments\n") + .append("- Immediate remediation steps\n") + .append("- Whether this specific image/tag is linked to online reports\n") + .append("- If not, reference Checkmarx attribution (per above rules)\n") + .append("- Never mention competitor vendors or tools\n\n"); + prompt.append("### Output Formatting\n\n") + .append("- Use Markdown: `##`, `- `, `**bold**`, `code`\n") + .append("- Developer-friendly tone, informative, concise\n") + .append("- No speculation - use only trusted, verified sources\n") + .append("- Include container-specific terminology and best practices\n"); + return prompt.toString(); + } + + /** + * builds the malicious content for a container prompt. + * + * @param prompt the prompt builder + * @param imageTag the image tag + */ + private static void buildMaliciousContentForContainerPrompt(StringBuilder prompt, String imageTag) { + prompt.append("### " + FIRECRACKER + " Malicious Container Detected\n\n") + .append("This container image has been flagged as **malicious**.\n\n") + .append("** " + WARNING + " Never deploy or use this container under any circumstances.**\n\n") + .append("#### " + SEARCH + " Investigation Guidelines:\n\n") + .append("- Search for trusted community or vendor reports about malicious activity involving this image\n") + .append("- If information exists about other tags but **not** tag `").append(imageTag).append("`, explicitly state:\n\n") + .append("> _\"This specific tag (`").append(imageTag).append("`) was identified as malicious by Checkmarx Security researchers.\"_\n\n") + .append("- If **no credible external information is found**, state:\n\n") + .append("> _\"This container image was identified as malicious by Checkmarx Security researchers based on internal threat intelligence and behavioral analysis.\"_\n\n") + .append("**Common Malicious Container Behaviors:**\n") + .append("- Data exfiltration to external servers\n") + .append("- Cryptocurrency mining operations\n") + .append("- Backdoor access establishment\n") + .append("- Credential harvesting\n") + .append("- Lateral movement within infrastructure\n\n") + .append("**Recommended Actions:**\n") + .append("- " + CHECK + " Immediately remove from all deployment pipelines\n") + .append("- " + CROSS + " Never redeploy or trust any version of this image\n") + .append("- " + REPEAT + " Replace with a well-known, secure alternative\n") + .append("- " + LOCK + " Audit all systems that may have run this container\n\n"); + + } + + /** + * Builds the vulnerability content for a container prompt. + * + * @param prompt the prompt builder + */ + private static void buildVulnerabilityContentForContainerPrompt(StringBuilder prompt) { + prompt.append("### " + POLICE_LIGHT + " Container Vulnerabilities\n\n") + .append("This container image contains known security vulnerabilities.\n\n") + .append("**Risk Assessment:**\n") + .append("- **Critical/High:** Immediate action required - vulnerable to active exploitation\n") + .append("- **Medium:** Should be addressed soon - potential for exploitation\n") + .append("- **Low:** Address when convenient - limited immediate risk\n\n") + .append("**Common Container Security Issues:**\n") + .append("- Outdated base images with known CVEs\n") + .append("- Unnecessary packages and services\n") + .append("- Running as root user\n") + .append("- Missing security patches\n") + .append("- Insecure default configurations\n\n"); + } + + /** + * Generates a detailed prompt for explaining an Infrastructure as Code (IaC) security issue. + * + * @param title The title of the IaC security issue. + * @param description A detailed description of the security issue. + * @param severity The severity level of the issue (e.g., High, Medium, Low). + * @param fileType The type of IaC file where the issue is detected (e.g., Terraform, YAML). + * @param expectedValue The expected secure value for the configuration. + * @param actualValue The actual insecure value in the configuration. + * @return A formatted Markdown prompt explaining the security issue, risks, and remediation steps. + */ + public static String buildIACExplanationPrompt(String title, String description, String severity, + String fileType, String expectedValue, String actualValue) { + + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the `").append(AGENT_NAME).append("`.\n\n"); + prompt.append("Your task is to **analyze and explain** the Infrastructure as Code (IaC) security issue: **") + .append(title).append("** with severity: `").append(severity).append("`.\n\n"); + + prompt.append("### " + EXCLAMATION + " Important Instructions:\n") + .append("- " + POINT_RIGHT + " **Do not change anything in the configuration. Just explain the risks and remediation steps.**\n") + .append("- " + CROSS + " **Never include references from Checkmarx competitors.**\n\n"); + + prompt.append("### " + SEARCH + " IaC Security Issue Overview\n\n") + .append("- **Issue:** `").append(title).append("`\n") + .append("- **File Type:** `").append(fileType).append("`\n") + .append("- **Severity:** `").append(severity).append("`\n") + .append("- **Description:** ").append(description).append("\n") + .append("- **Expected Value:** `").append(expectedValue).append("`\n") + .append("- **Actual Value:** `").append(actualValue).append("`\n\n"); + + prompt.append("### " + CONSTRUCTION + " Infrastructure Security Issue Analysis\n\n") + .append("**Issue Type:** Infrastructure Configuration Vulnerability\n\n"); + + prompt.append("### " + POLICE_LIGHT + " Security Risks\n\n") + .append("This configuration issue can lead to:\n") + .append("- **Critical/High:** Immediate security exposure - vulnerable to active exploitation\n") + .append("- **Medium:** Potential security risk - should be addressed soon\n") + .append("- **Low:** Security hygiene - address when convenient\n\n"); + + prompt.append("**Common IaC Security Issues:**\n") + .append("- Overly permissive access controls\n") + .append("- Exposed sensitive data or credentials\n") + .append("- Insecure network configurations\n") + .append("- Missing encryption settings\n") + .append("- Unrestricted public access\n") + .append("- Insecure service configurations\n\n"); + + prompt.append("### " + TOOLS + " Remediation Guidance\n\n") + .append("Offer actionable advice based on the file type:\n\n") + .append("**For ").append(fileType).append(" configurations:**\n") + .append("- Specific configuration changes needed\n") + .append("- Security best practices to follow\n") + .append("- Compliance considerations\n") + .append("- Testing and validation steps\n\n"); + + prompt.append("**Preventative Measures:**\n") + .append("- Use IaC security scanning in CI/CD pipelines\n") + .append("- Implement infrastructure policy as code\n") + .append("- Regular security audits of infrastructure\n") + .append("- Follow cloud provider security guidelines\n") + .append("- Use secure configuration templates\n\n"); + + prompt.append("### " + CHECK + " Summary Section\n\n") + .append("Conclude with:\n") + .append("- Overall risk explanation for infrastructure security\n") + .append("- Immediate remediation steps\n") + .append("- Impact on system security posture\n") + .append("- Long-term security considerations\n\n"); + + prompt.append("### " + PENCIL + " Output Formatting\n\n") + .append("- Use Markdown: `##`, `- `, `**bold**`, `code`\n") + .append("- Infrastructure-focused tone, informative, concise\n") + .append("- No speculation - use only trusted, verified sources\n") + .append("- Include infrastructure-specific terminology and best practices\n"); + return prompt.toString(); + } + + /** + * Builds a detailed prompt for explaining a security rule, including its description, + * severity, implications, and best practices for mitigation. + * + * @param ruleName The name of the security rule to explain. + * @param description A detailed description of the security issue. + * @param severity The severity level of the issue (e.g., High, Medium, Low). + * @return A formatted prompt string with the explanation, best practices, and additional resources related to the security rule. + */ + public static String buildASCAExplanationPrompt(String ruleName, String description, String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(AGENT_NAME).append(" providing detailed security explanations.\n\n") + .append("**Rule:** `").append(ruleName).append("` \n") + .append("**Severity:** `").append(severity).append("` \n") + .append("**Description:** ").append(description).append("\n\n") + .append("Please provide a comprehensive explanation of this security issue.\n\n"); + + prompt.append("### " + SEARCH + " Security Issue Overview\n\n") + .append("**Rule Name:** ").append(ruleName).append("\n") + .append("**Risk Level:** ").append(severity).append("\n\n") + .append("### " + OPEN_BOOK + " Detailed Explanation\n\n") + .append(description).append("\n\n") + .append("### " + WARNING + " Why This Matters\n\n") + .append("Explain the potential security implications:\n") + .append("- What attacks could exploit this vulnerability?\n") + .append("- What data or systems could be compromised?\n") + .append("- What is the potential business impact?\n\n") + .append("### " + SHIELD + " Security Best Practices\n\n") + .append("Provide general guidance on:\n") + .append("- How to prevent this type of issue\n") + .append("- Coding patterns to avoid\n") + .append("- Secure alternatives to recommend\n") + .append("- Tools and techniques for detection\n\n") + .append("### " + BOOKS + " Additional Resources\n\n") + .append("Suggest relevant:\n") + .append("- Security frameworks and standards\n") + .append("- Documentation and guides\n") + .append("- Tools for static analysis\n") + .append("- Training materials\n\n"); + + prompt.append("### " + PENCIL + " Output Format Guidelines\n\n") + .append("- Use clear, educational language\n") + .append("- Provide context for non-security experts\n") + .append("- Include practical examples where helpful\n") + .append("- Focus on actionable advice\n") + .append("- Be thorough but concise\n"); + return prompt.toString(); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java new file mode 100644 index 00000000..295e3bb3 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java new file mode 100644 index 00000000..2c996b92 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java new file mode 100644 index 00000000..c507d54c --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java new file mode 100644 index 00000000..e86f696e --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java new file mode 100644 index 00000000..4823b62e --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java new file mode 100644 index 00000000..7b5ef919 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java new file mode 100644 index 00000000..c1d9f48c --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java new file mode 100644 index 00000000..505aec53 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java new file mode 100644 index 00000000..8bd3a1df --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java new file mode 100644 index 00000000..add85055 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java new file mode 100644 index 00000000..716b5794 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java new file mode 100644 index 00000000..65180e2b --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java new file mode 100644 index 00000000..5f908056 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java new file mode 100644 index 00000000..44b395ee --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java new file mode 100644 index 00000000..1fba92d4 --- /dev/null +++ b/devassist-lib/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.common.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/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java new file mode 100644 index 00000000..b32d517a --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java @@ -0,0 +1,36 @@ +package com.checkmarx.eclipse.devassist.state; + +/** + * Enumeration of scan frequency options. + * Determines when scans are triggered automatically. + */ +public enum ScanFrequency { + ON_FILE_SAVE("on_save", "On File Save"), + ON_DOCUMENT_CHANGE("on_change", "On Document Change (1s debounce)"), + MANUAL_ONLY("manual", "Manual Only"); + + private final String key; + private final String label; + + ScanFrequency(String key, String label) { + this.key = key; + this.label = label; + } + + public String getKey() { + return key; + } + + public String getLabel() { + return label; + } + + public static ScanFrequency fromKey(String key) { + for (ScanFrequency freq : ScanFrequency.values()) { + if (freq.key.equals(key)) { + return freq; + } + } + return ON_DOCUMENT_CHANGE; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerState.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerState.java new file mode 100644 index 00000000..74cbd7ea --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerState.java @@ -0,0 +1,46 @@ +package com.checkmarx.eclipse.devassist.state; + +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the current state of scanner enable/disable settings. + * Holds which scanners are enabled and scan frequency preference. + */ +public class ScannerState { + + private final Map scannerStates = new HashMap<>(); + private ScanFrequency frequency; + + public ScannerState() { + initializeDefaults(); + } + + private void initializeDefaults() { + for (ScanEngine engine : ScanEngine.values()) { + scannerStates.put(engine, true); + } + this.frequency = ScanFrequency.ON_DOCUMENT_CHANGE; + } + + public boolean isEnabled(ScanEngine engine) { + return scannerStates.getOrDefault(engine, true); + } + + public void setEnabled(ScanEngine engine, boolean enabled) { + scannerStates.put(engine, enabled); + } + + public ScanFrequency getFrequency() { + return frequency; + } + + public void setFrequency(ScanFrequency frequency) { + this.frequency = frequency; + } + + public Map getAllStates() { + return new HashMap<>(scannerStates); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java new file mode 100644 index 00000000..7c1362eb --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java @@ -0,0 +1,75 @@ +package com.checkmarx.eclipse.devassist.state; + +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.ui.preferences.ScopedPreferenceStore; + +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +/** + * Manages scanner state persistence using Eclipse preferences. + * Loads and saves which scanners are enabled/disabled and scan frequency preference. + */ +public class ScannerStateManager { + + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; // Replace with your exact Bundle-SymbolicName if different + private static final String KEY_PREFIX = "scanner."; + private static final String KEY_ENABLED_SUFFIX = ".enabled"; + private static final String KEY_FREQUENCY = "scan.frequency"; + + private final IPreferenceStore prefs; + + public ScannerStateManager() { + this.prefs = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID); + } + + public ScannerStateManager(IPreferenceStore prefs) { + this.prefs = prefs; + } + + public ScannerState loadState() { + ScannerState state = new ScannerState(); + + for (ScanEngine engine : ScanEngine.values()) { + String key = getEnabledKey(engine); + boolean enabled = prefs.getBoolean(key); + state.setEnabled(engine, enabled); + } + + String freqKey = prefs.getString(KEY_FREQUENCY); + state.setFrequency(ScanFrequency.fromKey(freqKey)); + + return state; + } + + public void saveState(ScannerState state) { + for (ScanEngine engine : ScanEngine.values()) { + String key = getEnabledKey(engine); + boolean enabled = state.isEnabled(engine); + prefs.setValue(key, enabled); + } + + prefs.setValue(KEY_FREQUENCY, state.getFrequency().getKey()); + } + + public boolean isScannerEnabled(ScanEngine engine) { + return prefs.getBoolean(getEnabledKey(engine)); + } + + public void setScannerEnabled(ScanEngine engine, boolean enabled) { + prefs.setValue(getEnabledKey(engine), enabled); + } + + public ScanFrequency getScanFrequency() { + String key = prefs.getString(KEY_FREQUENCY); + return ScanFrequency.fromKey(key); + } + + public void setScanFrequency(ScanFrequency frequency) { + prefs.setValue(KEY_FREQUENCY, frequency.getKey()); + } + + private String getEnabledKey(ScanEngine engine) { + return KEY_PREFIX + engine.name().toLowerCase() + KEY_ENABLED_SUFFIX; + } +} \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java new file mode 100644 index 00000000..64b5ac14 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -0,0 +1,1562 @@ +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 org.eclipse.ui.plugin.AbstractUIPlugin; +import org.apache.commons.lang3.StringUtils; + +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsContentProvider; +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsLabelProvider; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.common.events.SettingsTopics; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.devassist.backend.Constants; +import com.checkmarx.eclipse.devassist.backend.listener.CheckmarxDocumentListener; +import com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; +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.remediation.RemediationManager; +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); + private static final Image CHECKMARX_OPEN_SETTINGS_LOGO = + AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, "/icons/checkmarx-80.png").createImage(); + + 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 = AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, 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 (StringUtils.isBlank(Preferences.getApiKey())) { + 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(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(Constants.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(Constants.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(SettingsTopics.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: opens the same preference page as the main results view + Action openPreferencesPageAction = new Action() { + @Override + public void run() { + PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn( + shell, "com.checkmarx.eclipse.properties.preferencespage", null, null); + if (pref != null) { + pref.open(); + } + } + }; + + // 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"); + } + + + } + + /** + * 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 + RealTimeScanJob scanJob = + new 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 + } + } + CheckmarxDocumentListener docListener = + new 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) { + new RemediationManager().viewDetails(issue, DevAssistConstants.QUICK_FIX); + } + }); + + // 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) { + new RemediationManager().fixWithCxOneAssist(issue, DevAssistConstants.QUICK_FIX); + } + }); + + // 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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java new file mode 100644 index 00000000..028e9561 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java new file mode 100644 index 00000000..9e2b7d21 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java new file mode 100644 index 00000000..5dc0a5f1 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java new file mode 100644 index 00000000..aebaeb3c --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java @@ -0,0 +1,39 @@ +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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java new file mode 100644 index 00000000..b1bed62e --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java new file mode 100644 index 00000000..1a996ca6 --- /dev/null +++ b/devassist-lib/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.devassist.backend.Constants; + +/** + * 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(Constants.MAIN_PLUGIN_ID, path); + imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java new file mode 100644 index 00000000..980648f1 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java new file mode 100644 index 00000000..94bd402d --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java new file mode 100644 index 00000000..edc9243c --- /dev/null +++ b/devassist-lib/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.common.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"; + public 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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java new file mode 100644 index 00000000..3c8cbaba --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java new file mode 100644 index 00000000..b0c4be77 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java new file mode 100644 index 00000000..7eaa61de --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java new file mode 100644 index 00000000..0d462a32 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java new file mode 100644 index 00000000..a7501a06 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java @@ -0,0 +1,106 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.realtime; +// +//import org.eclipse.core.resources.IFile; +//import org.eclipse.jface.text.DocumentEvent; +//import org.eclipse.jface.text.IDocumentListener; +// +//import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler; +// +///** +// * Real-time document listener for Checkmarx scanning. +// * +// * Equivalent to JetBrains' LocalInspectionTool.buildVisitor() — detects when +// * the user edits the currently opened file and triggers a real-time scan with +// * debounce (1 second of inactivity). +// * +// * This listener observes every keystroke and delegates to DevAssistScanScheduler +// * for debounced scanning coordination. +// */ +//public class CheckmarxDocumentListener implements IDocumentListener { +// +// private final RealTimeScanJob scanJob; +// private final IFile file; +// private final String fileName; +// private final DevAssistScanScheduler scheduler; +// private volatile boolean skipNextChange = false; +// private volatile long lastRescheduleTime = 0; +// +// /** +// * Create a document listener for a specific file. +// * +// * @param fileName the name of the file being edited (for logging) +// * @param scanJob the RealTimeScanJob to trigger on document changes +// * @param file the IFile being edited +// * @param scheduler the scheduler to coordinate scan rescheduling +// */ +// public CheckmarxDocumentListener(String fileName, RealTimeScanJob scanJob, IFile file, DevAssistScanScheduler scheduler) { +// this.fileName = fileName; +// this.scanJob = scanJob; +// this.file = file; +// this.scheduler = scheduler; +// } +// +// /** +// * Called when the document is about to be changed. +// * We don't need to do anything here, but we implement it for completeness. +// */ +// @Override +// public void documentAboutToBeChanged(DocumentEvent event) { +// // No action needed before change +// } +// +// /** +// * Called when the document has been changed. +// * Triggers the debounced real-time scan via DevAssistScanScheduler. +// * +// * This is equivalent to JetBrains' InspectionVisitor methods being called +// * during AST traversal — every edit triggers a potential scan. +// */ +// @Override +// public void documentChanged(DocumentEvent event) { +// try { +// // Skip rescheduling if this is a programmatic change (e.g., annotation updates) +// if (skipNextChange) { +// skipNextChange = false; +// return; +// } +// +// // Prevent StackOverflowError from rapid recursive reschedules +// long now = System.currentTimeMillis(); +// if (now - lastRescheduleTime < 100) { +// return; +// } +// lastRescheduleTime = now; +// +// // Reschedule the debounced scan job via scheduler +// // This cancels the previous job (if still scheduled) and starts a new 1-second timer +// if (scheduler != null && file != null) { +// scheduler.rescheduleInspection(file, 1000); // 1000ms = 1 second debounce +// } else if (scanJob != null) { +// // Fallback to direct reschedule if scheduler not available +// scanJob.reschedule(1000); +// } +// +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// +// public void setSkipNextChange(boolean skip) { +// this.skipNextChange = skip; +// } +// +// /** +// * Dispose this listener and clean up associated resources. +// * Call this when the editor is closed. +// */ +// public void dispose() { +// if (scanJob != null) { +// scanJob.cancel(); +// } +// } +// +// public String getFileName() { +// return fileName; +// } +//} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java new file mode 100644 index 00000000..6f7fe659 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java @@ -0,0 +1,408 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.realtime; +// +//import org.eclipse.ui.IEditorPart; +//import org.eclipse.ui.IPartListener2; +//import org.eclipse.ui.IWorkbenchPartReference; +//import org.eclipse.jface.text.IDocument; +//import org.eclipse.jface.text.source.ISourceViewer; +//import org.eclipse.ui.texteditor.ITextEditor; +//import org.eclipse.core.runtime.ILog; +//import org.eclipse.core.runtime.Platform; +//import org.eclipse.core.runtime.Status; +// +//import java.util.HashMap; +//import java.util.Map; +// +//import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +//import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +// +///** +// * Real-time editor listener for Checkmarx scanning. +// * +// * Equivalent to JetBrains' LocalInspectionTool integration — listens for editor +// * open/close events and registers document listeners for real-time scanning. +// * +// * When a text editor opens: +// * 1. Create a RealTimeScanJob for that file +// * 2. Register a CheckmarxDocumentListener on the document +// * 3. Every keystroke triggers the document listener +// * 4. Document listener reschedules the job (1-second debounce) +// * 5. When debounce expires, RealTimeScanJob.run() executes the scan +// * +// * When the editor closes: +// * - Dispose of the document listener and cancel the job +// */ +//public class CheckmarxEditorListener implements IPartListener2 { +// +// /** +// * Map of documents to their associated listeners. +// * Key: IDocument hash code (unique identifier for the document) +// * Value: CheckmarxDocumentListener (for cleanup on editor close) +// */ +// private final Map activeListeners = new HashMap<>(); +// +// /** +// * Map of documents to their associated scan jobs. +// * Key: IDocument hash code +// * Value: RealTimeScanJob (for cleanup and tracking) +// */ +// private final Map activeScanJobs = new HashMap<>(); +// +// public CheckmarxEditorListener() { +// +// } +// +// /** +// * Get the Eclipse log for this plugin. +// */ +// private ILog getLog() { +// return Platform.getLog(getClass()); +// } +// +// /** +// * Called when an editor part is opened. +// * Register real-time scanning for this editor. +// */ +// @Override +// public void partOpened(IWorkbenchPartReference partRef) { +// try { +// Object part = partRef.getPart(false); +// if (part instanceof IEditorPart) { +// setupRealtimeScanning((IEditorPart) part); +// } +// } catch (Exception e) { +// System.err.println("[REALTIME] Error in partOpened: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// /** +// * Called when an editor is activated. +// * Setup scanning if not done, or trigger rescan if switching to an already-open tab. +// */ +// @Override +// public void partActivated(IWorkbenchPartReference partRef) { +// try { +// Object part = partRef.getPart(false); +// if (part instanceof IEditorPart) { +// IEditorPart editor = (IEditorPart) part; +// IDocument document = getDocumentFromEditor(editor); +// if (document != null) { +// int documentId = document.hashCode(); +// // If already set up, trigger a rescan when user switches to tab +// if (activeListeners.containsKey(documentId)) { +// RealTimeScanJob scanJob = activeScanJobs.get(documentId); +// if (scanJob != null) { +// +// scanJob.reschedule(0); +// } +// return; +// } +// } +// // Not yet set up - do initial setup +// setupRealtimeScanning(editor); +// } +// } catch (Exception e) { +// System.err.println("[REALTIME] Error in partActivated: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// /** +// * Called when an editor is closed. +// * Clean up document listeners and cancel pending scan jobs. +// */ +// @Override +// public void partClosed(IWorkbenchPartReference partRef) { +// try { +// Object part = partRef.getPart(false); +// if (part instanceof IEditorPart) { +// cleanupRealtimeScanning((IEditorPart) part); +// } +// } catch (Exception e) { +// System.err.println("[REALTIME] Error in partClosed: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// /** +// * Setup real-time scanning on the given editor. +// * +// * @param editor the editor part (should be a text editor) +// */ +// private void setupRealtimeScanning(IEditorPart editor) { +// if (editor == null) { +// return; +// } +// +// // Get the document from the editor +// IDocument document = getDocumentFromEditor(editor); +// if (document == null) { +// // Not a text editor or no document available +// return; +// } +// +// // Use document hash code as a unique identifier +// int documentId = document.hashCode(); +// +// // Check if we've already set up scanning for this document +// if (activeListeners.containsKey(documentId)) { +// +// return; +// } +// +// // Get file name for logging +// String fileName = extractFileNameFromEditor(editor); +// +// +// // Log to Eclipse Error Log +// String message = "User opened the file: " + fileName; +// getLog().log(new Status(Status.INFO, "com.checkmarx.eclipse.plugin", message)); +// +// // Create a scan job for this file +// // Note: We extract the IFile from the editor if possible, otherwise use null +// // (The actual file can be obtained from the editor input) +// org.eclipse.core.resources.IFile file = extractFileFromEditor(editor); +// RealTimeScanJob scanJob = new RealTimeScanJob(file, fileName); +// +// // Get the scheduler from project session properties +// com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = null; +// if (file != null) { +// try { +// org.eclipse.core.resources.IProject project = file.getProject(); +// if (project != null) { +// scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project.getSessionProperty( +// new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "scan-scheduler")); +// } +// } catch (Exception e) { +// +// } +// } +// +// // Create a document listener that will reschedule the job on every keystroke +// CheckmarxDocumentListener docListener = new CheckmarxDocumentListener(fileName, scanJob, file, scheduler); +// +// // Register the document listener +// try { +// document.addDocumentListener(docListener); +// +// // Store the listener and job for later cleanup +// activeListeners.put(documentId, docListener); +// activeScanJobs.put(documentId, scanJob); +// +// +// +// // **CRITICAL FIX: Apply cached decorations if findings exist for this file** +// // JetBrains pattern: when editor opens, apply cached decorations immediately +// // This fixes the issue where decorations don't appear if editor wasn't open during scan +// applyCachedDecorationsForFile(file, document); +// +// // **CRITICAL FIX: Trigger initial scan when file is opened** +// // JetBrains pattern: scan on file open, then on keystroke debounce +// // Without this, opening a file doesn't trigger any scan — only edits do +// +// scanJob.reschedule(0); +// +// } catch (Exception e) { +// System.err.println("[REALTIME] ✗ Error registering document listener: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// /** +// * Cleanup real-time scanning on the given editor. +// * +// * @param editor the editor part being closed +// */ +// private void cleanupRealtimeScanning(IEditorPart editor) { +// if (editor == null) { +// return; +// } +// +// // Get the document from the editor +// IDocument document = getDocumentFromEditor(editor); +// if (document == null) { +// return; +// } +// +// int documentId = document.hashCode(); +// +// // Remove the document listener +// CheckmarxDocumentListener listener = activeListeners.remove(documentId); +// if (listener != null) { +// try { +// document.removeDocumentListener(listener); +// listener.dispose(); +// +// } catch (Exception e) { +// System.err.println("[REALTIME] Error removing document listener: " + e.getMessage()); +// } +// } +// +// // Cancel the scan job +// RealTimeScanJob scanJob = activeScanJobs.remove(documentId); +// if (scanJob != null) { +// scanJob.cancel(); +// +// } +// } +// +// /** +// * Extract the IDocument from an editor. +// * Handles both standard ITextEditor and editors like MavenPomEditor. +// * +// * @param editor the editor part +// * @return the document, or null if not available +// */ +// private IDocument getDocumentFromEditor(IEditorPart editor) { +// if (editor == null) { +// return null; +// } +// +// // Try method 1: Direct ITextEditor instance +// if (editor instanceof ITextEditor) { +// ITextEditor textEditor = (ITextEditor) editor; +// try { +// return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); +// } catch (Exception e) { +// // Fall through to try adapter pattern +// } +// } +// +// // Try method 2: Adapter pattern (for MavenPomEditor and other non-ITextEditor editors) +// try { +// ITextEditor textEditor = editor.getAdapter(ITextEditor.class); +// if (textEditor != null) { +// return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); +// } +// } catch (Exception e) { +// // Fall through to next method +// } +// +// // Try method 3: Direct IDocument adapter (some editors provide this) +// try { +// IDocument document = editor.getAdapter(IDocument.class); +// if (document != null) { +// return document; +// } +// } catch (Exception e) { +// // Fall through +// } +// +// return null; +// } +// +// /** +// * Extract the file name from an editor for logging. +// * +// * @param editor the editor part +// * @return the file name, or "unknown" if not available +// */ +// private String extractFileNameFromEditor(IEditorPart editor) { +// try { +// return editor.getEditorInput().getName(); +// } catch (Exception e) { +// return "unknown"; +// } +// } +// +// /** +// * Extract the IFile from an editor (may return null for non-workspace files). +// * +// * @param editor the editor part +// * @return the IFile, or null if not available +// */ +// private org.eclipse.core.resources.IFile extractFileFromEditor(IEditorPart editor) { +// try { +// if (editor.getEditorInput() instanceof org.eclipse.ui.part.FileEditorInput) { +// org.eclipse.ui.part.FileEditorInput fileInput = +// (org.eclipse.ui.part.FileEditorInput) editor.getEditorInput(); +// return fileInput.getFile(); +// } +// } catch (Exception e) { +// // Ignore exceptions; file extraction is optional +// } +// return null; +// } +// +// /** +// * Apply cached decorations (gutter icons, underlines) when editor opens. +// * +// * JetBrains pattern: when an editor opens, check if there are cached findings +// * and apply decorations immediately. This ensures decorations appear even if +// * the editor wasn't open when the scan completed. +// * +// * @param file the Eclipse IFile being opened +// * @param document the document for the file +// */ +// private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, IDocument document) { +// if (file == null || document == null) { +// return; +// } +// +// try { +// String filePath = file.getLocation().toOSString(); +// org.eclipse.core.resources.IProject project = file.getProject(); +// +// if (project == null) { +// return; +// } +// +// // Get cached findings for this file +// ProblemHolderService problemHolder = +// (ProblemHolderService) project.getSessionProperty( +// new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); +// +// if (problemHolder == null) { +// return; +// } +// +// java.util.List cachedIssues = +// problemHolder.getScanIssuesByFile(filePath); +// +// if (cachedIssues == null || cachedIssues.isEmpty()) { +// +// return; +// } +// +// // Apply decorations for cached findings +// +// ProblemDecorator.decorateEditor(file, cachedIssues); +// +// } catch (Exception e) { +// System.err.println("[REALTIME] Error applying cached decorations: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// // Implement other IPartListener2 methods (not used for real-time scanning) +// +// @Override +// public void partBroughtToTop(IWorkbenchPartReference partRef) {} +// +// @Override +// public void partDeactivated(IWorkbenchPartReference partRef) {} +// +// @Override +// public void partHidden(IWorkbenchPartReference partRef) {} +// +// @Override +// public void partVisible(IWorkbenchPartReference partRef) {} +// +// @Override +// public void partInputChanged(IWorkbenchPartReference partRef) {} +// +// /** +// * Get the number of active listeners (for testing/debugging). +// */ +// public int getActiveListenerCount() { +// return activeListeners.size(); +// } +// +// /** +// * Get the number of active scan jobs (for testing/debugging). +// */ +// public int getActiveScanJobCount() { +// return activeScanJobs.size(); +// } +//} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java new file mode 100644 index 00000000..cb34ffc8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java @@ -0,0 +1,240 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.realtime; +// +//import org.eclipse.core.resources.IFile; +//import org.eclipse.core.runtime.IProgressMonitor; +//import org.eclipse.core.runtime.IStatus; +//import org.eclipse.core.runtime.Status; +//import org.eclipse.core.runtime.jobs.Job; +//import org.eclipse.core.runtime.ILog; +//import org.eclipse.core.runtime.Platform; +// +///** +// * Real-time scan job with debounce support. +// * +// * When the user edits a file, CheckmarxDocumentListener calls reschedule() repeatedly +// * as the user types. This job cancels the previous scheduled execution and starts a +// * new 1-second timer, so the scan only runs after the user pauses typing. +// * +// * Equivalent to: +// * - JetBrains' real-time inspection pipeline (with debounce built-in) +// * - Eclipse's incremental builder, but for on-demand scanning +// * +// * This is a background Job, so it runs off the UI thread and won't freeze the editor. +// */ +//public class RealTimeScanJob extends Job { +// +// private final IFile file; +// private final String fileName; +// +// // Store the timestamp when the user last made changes +// private long lastChangeTime = System.currentTimeMillis(); +// +// /** +// * Create a real-time scan job for a specific file. +// * +// * @param file the IFile resource to scan +// * @param fileName the file name (for logging) +// */ +// public RealTimeScanJob(IFile file, String fileName) { +// super("Checkmarx Real-Time Scan: " + fileName); +// this.file = file; +// this.fileName = fileName; +// +// // Configure job properties for background execution +// setSystem(false); // Show in progress view +// setPriority(Job.DECORATE); // Lower priority than user interactions +// setUser(false); // Not a user-initiated job +// +// +// } +// +// /** +// * Get the Eclipse log for this plugin. +// */ +// private ILog getLog() { +// return Platform.getLog(getClass()); +// } +// +// /** +// * Reschedule this job with a given delay (debounce). +// * +// * If the job is already scheduled, it is cancelled and rescheduled with a new delay. +// * This ensures the scan only runs after the user stops typing for the specified delay. +// * +// * @param delayMs delay in milliseconds before the job should run +// */ +// public synchronized void reschedule(long delayMs) { +// // Update the last change time +// this.lastChangeTime = System.currentTimeMillis(); +// +// // Cancel any previously scheduled execution +// cancel(); +// +// // Schedule the job to run after the delay +// schedule(delayMs); +// +// +// } +// +// /** +// * Run the real-time scan. +// * +// * This method is called by the Eclipse Jobs framework after the debounce delay expires. +// * It performs the actual scanning logic. +// * +// * Currently, this just logs a message. In production, you would: +// * 1. Parse the file +// * 2. Run security checks (synchronously or via backend API) +// * 3. Create markers for problems found +// * 4. Update the editor decoration +// * +// * @param monitor progress monitor for cancellation support +// * @return Status.OK if successful, Status.CANCEL if cancelled +// */ +// @Override +// protected IStatus run(IProgressMonitor monitor) { +// try { +// // Check if file still exists and is accessible +// if (file == null || !file.exists()) { +// +// return Status.CANCEL_STATUS; +// } +// +// // Check if the job was cancelled while waiting +// if (monitor.isCanceled()) { +// +// return Status.CANCEL_STATUS; +// } +// +// // **STEP 1: Check authentication status** +// if (!isUserAuthenticated()) { +// +// +// return Status.OK_STATUS; // Return OK but don't scan +// } +// +// +// +// +// +// +// // Call our backend scanners via ScanManager +// try { +// org.eclipse.core.resources.IProject project = file.getProject(); +// if (project == null || !project.isOpen()) { +// +// return Status.OK_STATUS; +// } +// +// String projectName = project.getName(); +// org.eclipse.core.runtime.QualifiedName registryKey = new org.eclipse.core.runtime.QualifiedName( +// "com.checkmarx.eclipse.plugin", "scanner-registry"); +// org.eclipse.core.runtime.QualifiedName stateHolderKey = new org.eclipse.core.runtime.QualifiedName( +// "com.checkmarx.eclipse.plugin", "state-holder"); +// +// // Get or lazily initialize backend services +// com.checkmarx.eclipse.devassist.backend.ScannerRegistry registry = +// (com.checkmarx.eclipse.devassist.backend.ScannerRegistry) +// project.getSessionProperty(registryKey); +// +// com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder = +// (com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder) +// project.getSessionProperty(stateHolderKey); +// +// // Lazy initialization if not found +// if (registry == null) { +// +// registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project); +// project.setSessionProperty(registryKey, registry); +// +// } +// +// if (stateHolder == null) { +// +// stateHolder = new com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder(); +// project.setSessionProperty(stateHolderKey, stateHolder); +// +// } +// +// // Execute backend scanners +// +// com.checkmarx.eclipse.devassist.common.ScanManager scanManager = +// new com.checkmarx.eclipse.devassist.common.ScanManager(registry, stateHolder); +// +// String filePath = file.getLocation().toOSString(); +// +// +// java.util.List issues = +// scanManager.scanFile(filePath); +// +// +// for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : issues) { +// } +// +// // Publish results to UI +// +// if (!issues.isEmpty()) { +// com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues); +// +// } else { +// +// } +// +// } catch (Exception e) { +// System.err.println("[REALTIME] ✗ ERROR in step above: " + e.getMessage()); +// e.printStackTrace(); +// System.err.println("[REALTIME] Stack trace:"); +// for (StackTraceElement elem : e.getStackTrace()) { +// System.err.println("[REALTIME] at " + elem); +// } +// } +// +// +// return Status.OK_STATUS; +// +// } catch (Exception e) { +// System.err.println("[REALTIME] ✗ UNEXPECTED ERROR during real-time scan: " + e.getMessage()); +// e.printStackTrace(); +// System.err.println("[REALTIME] Full stack trace:"); +// for (StackTraceElement elem : e.getStackTrace()) { +// System.err.println("[REALTIME] at " + elem); +// } +// // Return error status but don't fail the job permanently +// return new Status(IStatus.WARNING, "com.checkmarx.eclipse.plugin", +// "Real-time scan failed for " + fileName, e); +// } +// } +// +// /** +// * Check if user is authenticated by checking if API key is configured. +// */ +// private boolean isUserAuthenticated() { +// String apiKey = com.checkmarx.eclipse.common.properties.Preferences.getApiKey(); +// return apiKey != null && !apiKey.trim().isEmpty(); +// } +// +// @Override +// public boolean belongsTo(Object family) { +// // Group all Checkmarx real-time scan jobs together +// // This allows Eclipse to cancel all scans at once if needed +// return family != null && family.equals("com.checkmarx.realtime.scan"); +// } +// +// /** +// * Called when the job is cancelled. +// * Cleanup any resources if needed. +// */ +// @Override +// protected void canceling() { +// +// super.canceling(); +// } +// +// public String getFileName() { +// return fileName; +// } +// +// public IFile getFile() { +// return file; +// } +//} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java new file mode 100644 index 00000000..31584f54 --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java new file mode 100644 index 00000000..7881693f --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java new file mode 100644 index 00000000..486a986e --- /dev/null +++ b/devassist-lib/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/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/preferences/WelcomeDialog.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/preferences/WelcomeDialog.java new file mode 100644 index 00000000..e562d817 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/preferences/WelcomeDialog.java @@ -0,0 +1,470 @@ +package com.checkmarx.eclipse.devassist.ui.preferences; + +import org.eclipse.e4.core.services.events.IEventBroker; +import org.eclipse.e4.ui.css.swt.theme.ITheme; +import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.services.IServiceLocator; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.service.event.EventHandler; +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Welcome dialog for Checkmarx Eclipse plugin. + * Displayed after successful authentication to inform users about key features. + */ +public class WelcomeDialog extends TitleAreaDialog { + + private static final int DIALOG_WIDTH = 800; + private static final int DIALOG_HEIGHT = 530; + private static final int WRAP_WIDTH = 250; + private static final int BULLET_INDENT = 20; + private static final int CONTENT_MARGIN = 20; + private static final int IMAGE_PANEL_WIDTH = 380; + private static final String SCANNER_IMAGE_PATH = "icons/welcomePageScanner.svg"; + private static final String SCANNER_IMAGE_PATH_DARK = "icons/welcomePageScanner_dark.svg"; + // Key the e4 CSS theme engine itself is registered under on the Display; + // this is the same lookup org.eclipse.e4.ui.css.swt.internal.theme.ThemeEngineManager + // uses internally, so it reflects Eclipse's actual active theme (not a guess). + private static final String THEME_ENGINE_DISPLAY_KEY = "org.eclipse.e4.ui.css.swt.theme"; + private static final String DARK_THEME_ID_FRAGMENT = "dark"; + + private final boolean mcpEnabled; + private Button realTimeScannersCheckbox; + private final RealTimeSettingsManager settingsManager; + private Image scannerImage; + private Label scannerImageLabel; + private IEventBroker themeEventBroker; + private EventHandler themeChangeHandler; + + /** + * Constructor + * @param parentShell the parent shell + * @param mcpEnabled whether MCP is enabled for the tenant + */ + public WelcomeDialog(Shell parentShell, boolean mcpEnabled) { + this(parentShell, mcpEnabled, new DefaultRealTimeSettingsManager()); + } + + /** + * Constructor with dependency injection for testability + * @param parentShell the parent shell + * @param mcpEnabled whether MCP is enabled for the tenant + * @param settingsManager manager for real-time settings + */ + public WelcomeDialog(Shell parentShell, boolean mcpEnabled, RealTimeSettingsManager settingsManager) { + super(parentShell); + this.mcpEnabled = mcpEnabled; + this.settingsManager = settingsManager; + // Deliberately not adding SWT.RESIZE: the dialog is sized to show every + // section at once, so resizing (which could clip content again) is disabled. + + // Log MCP status for debugging + String mcpStatus = mcpEnabled ? "ENABLED" : "DISABLED"; + CxLogger.info("[WELCOME] MCP status: " + mcpStatus); + } + + @Override + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText("Checkmarx"); + shell.setSize(DIALOG_WIDTH, DIALOG_HEIGHT); + } + + @Override + protected Control createDialogArea(Composite parent) { + setTitle(DevAssistConstants.WELCOME_TITLE); + setMessage(DevAssistConstants.WELCOME_SUBTITLE); + setTitleImage(null); // Remove title image for cleaner look + + Composite container = (Composite) super.createDialogArea(parent); + container.setLayout(new GridLayout(1, false)); + + createContentArea(container); + + return container; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, OK, DevAssistConstants.WELCOME_CLOSE_BUTTON, true); + } + + private void createContentArea(Composite container) { + + // Everything below is laid out directly (no scrolled composite) so that, + // combined with the fixed, non-resizable dialog size, all content is + // visible at once without scrolling or clipping. + Composite mainRow = new Composite(container, SWT.NONE); + GridLayout rowLayout = new GridLayout(2, false); + rowLayout.marginLeft = CONTENT_MARGIN; + rowLayout.marginRight = CONTENT_MARGIN; + rowLayout.marginTop = CONTENT_MARGIN; + rowLayout.marginBottom = CONTENT_MARGIN; + rowLayout.horizontalSpacing = 20; + mainRow.setLayout(rowLayout); + mainRow.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + // Left column - Feature card + main bullets, stacked + Composite leftColumn = new Composite(mainRow, SWT.NONE); + GridLayout leftLayout = new GridLayout(1, false); + leftLayout.marginWidth = 0; + leftLayout.marginHeight = 0; + leftLayout.verticalSpacing = 8; + leftColumn.setLayout(leftLayout); + leftColumn.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + + if (mcpEnabled) { + addFeatureCard(leftColumn); + } + + addBullet(leftColumn, DevAssistConstants.WELCOME_MAIN_FEATURE_1); + addBullet(leftColumn, DevAssistConstants.WELCOME_MAIN_FEATURE_2); + addBullet(leftColumn, DevAssistConstants.WELCOME_MAIN_FEATURE_3); + addBullet(leftColumn, DevAssistConstants.WELCOME_MAIN_FEATURE_4); + + // Right column - scanner image + createScannerImage(mainRow); + } + + private void createScannerImage(Composite container) { + Composite rightPanel = new Composite(container, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.marginWidth = 0; + layout.marginHeight = 0; + rightPanel.setLayout(layout); + GridData gd = new GridData(SWT.CENTER, SWT.TOP, true, false); + gd.widthHint = IMAGE_PANEL_WIDTH; + rightPanel.setLayoutData(gd); + + scannerImageLabel = new Label(rightPanel, SWT.CENTER); + scannerImageLabel.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, false)); + + scannerImage = loadScannerImage(); + if (scannerImage != null) { + scannerImageLabel.setImage(scannerImage); + } + scannerImageLabel.addDisposeListener(e -> { + unsubscribeThemeChangeListener(); + if (scannerImage != null && !scannerImage.isDisposed()) { + scannerImage.dispose(); + } + }); + + registerThemeChangeListener(); + } + + private Image loadScannerImage() { + String path = isDarkTheme() ? SCANNER_IMAGE_PATH_DARK : SCANNER_IMAGE_PATH; + try { + // devassist-lib bundle symbolic name + ImageDescriptor descriptor = AbstractUIPlugin.imageDescriptorFromPlugin("com.checkmarx.eclipse.devassist", path); + if (descriptor != null) { + return descriptor.createImage(); + } + } catch (Exception e) { + CxLogger.error("Failed to load welcome scanner image", e); + } + return null; + } + + /** + * Reads Eclipse's own e4 CSS theme engine - the same mechanism the Platform + * uses to decide dark vs. light styling - so the scanner image always matches + * whatever theme Eclipse is actually rendering with, instead of guessing from + * a color sample (which broke down in practice, e.g. custom/high-contrast themes). + */ + private boolean isDarkTheme() { + ITheme activeTheme = getActiveTheme(); + if (activeTheme != null && activeTheme.getId() != null) { + return activeTheme.getId().toLowerCase().contains(DARK_THEME_ID_FRAGMENT); + } + return isDarkByBackgroundLuminance(); + } + + private ITheme getActiveTheme() { + try { + Display display = Display.getCurrent(); + Object engineData = display != null ? display.getData(THEME_ENGINE_DISPLAY_KEY) : null; + if (engineData instanceof IThemeEngine) { + return ((IThemeEngine) engineData).getActiveTheme(); + } + } catch (Throwable t) { + // e4 CSS theming bundle not present/active in this runtime; caller falls back. + CxLogger.error("Eclipse e4 theme engine unavailable, falling back to color heuristic", + t instanceof Exception ? (Exception) t : new Exception(t)); + } + return null; + } + + /** + * Fallback for the rare runtime where the e4 CSS theme engine isn't registered + * on the Display: approximate dark mode from the widget background luminance. + */ + private boolean isDarkByBackgroundLuminance() { + Color background = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); + double luminance = (0.299 * background.getRed() + 0.587 * background.getGreen() + 0.114 * background.getBlue()) / 255.0; + return luminance < 0.5; + } + + /** + * Keeps the scanner image correct if the user flips Eclipse's theme (Preferences > + * General > Appearance) while this dialog happens to be open, instead of only + * checking the theme once at open time. + */ + private void registerThemeChangeListener() { + try { + // Get event broker from OSGi service registry via PlatformUI + Object serviceLocator = PlatformUI.getWorkbench(); + if (serviceLocator instanceof IServiceLocator) { + themeEventBroker = ((IServiceLocator) serviceLocator).getService(IEventBroker.class); + } + + if (themeEventBroker != null) { + themeChangeHandler = event -> Display.getDefault().asyncExec(this::refreshScannerImageForThemeChange); + themeEventBroker.subscribe(IThemeEngine.Events.THEME_CHANGED, themeChangeHandler); + } + } catch (Exception e) { + CxLogger.error("Failed to subscribe to Eclipse theme change events", e); + } + } + + private void unsubscribeThemeChangeListener() { + if (themeEventBroker != null && themeChangeHandler != null) { + themeEventBroker.unsubscribe(themeChangeHandler); + } + themeEventBroker = null; + themeChangeHandler = null; + } + + private void refreshScannerImageForThemeChange() { + if (scannerImageLabel == null || scannerImageLabel.isDisposed()) { + return; + } + Image newImage = loadScannerImage(); + Image oldImage = scannerImage; + scannerImage = newImage; + scannerImageLabel.setImage(newImage); + scannerImageLabel.getParent().layout(); + if (oldImage != null && !oldImage.isDisposed()) { + oldImage.dispose(); + } + } + + private void addFeatureCard(Composite parent) { + Composite card = new Composite(parent, SWT.BORDER); + GridLayout layout = new GridLayout(1, false); + layout.marginLeft = 10; + layout.marginRight = 10; + layout.marginTop = 10; + layout.marginBottom = 10; + layout.verticalSpacing = 4; + card.setLayout(layout); + card.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + + // Set subtle background color + Color bgColor = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); + card.setBackground(bgColor); + + // Card header with checkbox + createCardHeader(card); + + // Card features + addBullet(card, DevAssistConstants.WELCOME_ASSIST_FEATURE_1); + addBullet(card, DevAssistConstants.WELCOME_ASSIST_FEATURE_2); + addBullet(card, DevAssistConstants.WELCOME_ASSIST_FEATURE_3); + + if (mcpEnabled) { + addBullet(card, DevAssistConstants.WELCOME_MCP_INSTALLED_INFO); + } + } + + private void createCardHeader(Composite parent) { + Composite header = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.marginLeft = 0; + layout.marginRight = 0; + layout.horizontalSpacing = 6; + header.setLayout(layout); + header.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + header.setBackground(parent.getBackground()); + + realTimeScannersCheckbox = new Button(header, SWT.CHECK); + realTimeScannersCheckbox.setEnabled(mcpEnabled); + realTimeScannersCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + Label titleLabel = new Label(header, SWT.NONE); + titleLabel.setText(DevAssistConstants.WELCOME_ASSIST_TITLE); + titleLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + FontData fontData = titleLabel.getFont().getFontData()[0]; + fontData.setStyle(SWT.BOLD); + Font boldFont = new Font(Display.getCurrent(), fontData); + titleLabel.setFont(boldFont); + titleLabel.setBackground(parent.getBackground()); + titleLabel.addDisposeListener(e -> boldFont.dispose()); + + header.setBackground(parent.getBackground()); + + // Configure checkbox behavior + configureCheckboxBehavior(); + refreshCheckboxState(); + } + + private void addBullet(Composite parent, String text) { + Composite bulletPanel = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.marginLeft = BULLET_INDENT; + layout.marginRight = 0; + layout.marginTop = 0; + layout.marginBottom = 0; + layout.horizontalSpacing = 6; + bulletPanel.setLayout(layout); + bulletPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + + // Bullet point + Label bulletLabel = new Label(bulletPanel, SWT.NONE); + bulletLabel.setText("•"); + bulletLabel.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + + FontData fontData = bulletLabel.getFont().getFontData()[0]; + fontData.setStyle(SWT.BOLD); + Font boldFont = new Font(Display.getCurrent(), fontData); + bulletLabel.setFont(boldFont); + bulletLabel.addDisposeListener(e -> boldFont.dispose()); + + // Text with wrapping + Label textLabel = new Label(bulletPanel, SWT.WRAP); + textLabel.setText(text); + GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false); + gd.widthHint = WRAP_WIDTH; + textLabel.setLayoutData(gd); + } + + private void configureCheckboxBehavior() { + if (realTimeScannersCheckbox == null) return; + + realTimeScannersCheckbox.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + boolean anyCurrentlyEnabled = settingsManager.areAnyEnabled(); + settingsManager.setAll(!anyCurrentlyEnabled); + refreshCheckboxState(); + } + }); + } + + private void refreshCheckboxState() { + if (realTimeScannersCheckbox == null) return; + + boolean anyEnabled = settingsManager.areAnyEnabled(); + realTimeScannersCheckbox.setSelection(anyEnabled); + updateCheckboxTooltip(); + } + + private void updateCheckboxTooltip() { + if (realTimeScannersCheckbox == null) return; + + if (!mcpEnabled) { + realTimeScannersCheckbox.setToolTipText("Checkmarx MCP is not enabled for this tenant."); + return; + } + + boolean allEnabled = settingsManager.areAllEnabled(); + boolean anyEnabled = settingsManager.areAnyEnabled(); + + String tooltipText; + if (allEnabled) { + tooltipText = "Disable all real-time scanners"; + } else if (anyEnabled) { + tooltipText = "Some scanners are enabled. Click to enable all real-time scanners"; + } else { + tooltipText = "Enable all real-time scanners"; + } + realTimeScannersCheckbox.setToolTipText(tooltipText); + } + + @Override + protected void okPressed() { + super.okPressed(); + } + + @Override + protected Point getInitialSize() { + return new Point(DIALOG_WIDTH, DIALOG_HEIGHT); + } + + /** + * Get the real-time scanners checkbox (for testing purposes) + */ + public Button getRealTimeScannersCheckbox() { + return realTimeScannersCheckbox; + } + + /** + * Manager interface for real-time settings + */ + public interface RealTimeSettingsManager { + boolean areAllEnabled(); + boolean areAnyEnabled(); + void setAll(boolean enable); + } + + /** + * Default implementation using ScannerStateManager for persistence + */ + private static class DefaultRealTimeSettingsManager implements RealTimeSettingsManager { + private final com.checkmarx.eclipse.devassist.state.ScannerStateManager stateManager; + private com.checkmarx.eclipse.devassist.state.ScannerState currentState; + + DefaultRealTimeSettingsManager() { + this.stateManager = new com.checkmarx.eclipse.devassist.state.ScannerStateManager(); + this.currentState = stateManager.loadState(); + } + + @Override + public boolean areAllEnabled() { + for (com.checkmarx.eclipse.devassist.model.ScanEngine engine : com.checkmarx.eclipse.devassist.model.ScanEngine.values()) { + if (!currentState.isEnabled(engine)) { + return false; + } + } + return true; + } + + @Override + public boolean areAnyEnabled() { + for (com.checkmarx.eclipse.devassist.model.ScanEngine engine : com.checkmarx.eclipse.devassist.model.ScanEngine.values()) { + if (currentState.isEnabled(engine)) { + return true; + } + } + return false; + } + + @Override + public void setAll(boolean enable) { + for (com.checkmarx.eclipse.devassist.model.ScanEngine engine : com.checkmarx.eclipse.devassist.model.ScanEngine.values()) { + currentState.setEnabled(engine, enable); + } + stateManager.saveState(currentState); + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java new file mode 100644 index 00000000..6381aa30 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java @@ -0,0 +1,174 @@ +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"; + + /******************************** WELCOME DIALOG ********************************/ + public static final String WELCOME_TITLE = "Welcome to Checkmarx"; + public static final String WELCOME_SUBTITLE = "Checkmarx offers immediate threat detection and assists you in preventing vulnerabilities before they arise."; + public static final String WELCOME_ASSIST_TITLE = "Code Smarter with Checkmarx One Assist"; + public static final String WELCOME_ASSIST_FEATURE_1 = "Get instant security feedback as you code."; + public static final String WELCOME_ASSIST_FEATURE_2 = "See suggested fixes for vulnerabilities across open source, config, and code."; + public static final String WELCOME_ASSIST_FEATURE_3 = "Fix faster with intelligent, context-aware remediation inside your IDE."; + public static final String WELCOME_MAIN_FEATURE_1 = "Run SAST, SCA, IaC, Containers and Secrets scans."; + public static final String WELCOME_MAIN_FEATURE_2 = "Create a new Checkmarx branch from your local workspace."; + public static final String WELCOME_MAIN_FEATURE_3 = "Preview or rescan before committing."; + public static final String WELCOME_MAIN_FEATURE_4 = "Triage & fix issues directly in the editor."; + public static final String WELCOME_CLOSE_BUTTON = "Close"; + public static final String WELCOME_MCP_INSTALLED_INFO = "Checkmarx MCP Installed automatically - no need for manual integration"; + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java new file mode 100644 index 00000000..41f67e2b --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -0,0 +1,343 @@ +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.dnd.Transfer; +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 org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.remediation.NotificationPopup; +import com.checkmarx.eclipse.common.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]; + } + + public static String getAgentName() { + // TODO Auto-generated method stub + return DevAssistConstants.CX_AGENT_NAME; + } + /** + * Returns the vulnerability details for the given vulnerability id. + * + * @param scanIssue scan issue containing vulnerabilities details + * @param vulnerabilityId - vulnerability id + * @return Vulnerability - vulnerability details + */ + public static Vulnerability getVulnerabilityDetails(ScanIssue scanIssue, String vulnerabilityId) { + if (Objects.isNull(scanIssue.getVulnerabilities()) || scanIssue.getVulnerabilities().isEmpty()) { + CxLogger.warning(String.format("No vulnerabilities found in scan issue object for scan engine: %s.", scanIssue.getScanEngine().name())); + return null; + } + return scanIssue.getVulnerabilities().stream() + .filter(vulnerability -> vulnerability.getVulnerabilityId().equals(vulnerabilityId)) + .findFirst() + .orElse(null); + } + + /** + * Copies text to the system clipboard. + * + * @param text the text to copy + * @return true if successful, false otherwise + */ + public static boolean copyToClipboard(String text) { + try { + Display display = Display.getDefault(); + display.syncExec(() -> { + Clipboard clipboard = new Clipboard(display); + try { + clipboard.setContents(new Object[] { text }, new Transfer[] { TextTransfer.getInstance() }); + } finally { + clipboard.dispose(); + } + }); + CxLogger.info("CX#: Content copied to clipboard"); + return true; + } catch (Exception e) { + CxLogger.error("CX#: Failed to copy to clipboard: " + e.getMessage(), e); + return false; + } + } + + + /** + * Copies the given text to the system clipboard and shows a standard + * Eclipse notification popup confirming the action. + */ + public static boolean copyToClipboardWithNotification(String notificationMessage, String notificationTitle) { + try { + Display display = Display.getCurrent() != null ? Display.getCurrent() : Display.getDefault(); + + // 1. Copy to clipboard + Clipboard clipboard = new Clipboard(display); + try { + clipboard.setContents(new Object[] { notificationMessage }, + new Transfer[] { TextTransfer.getInstance() }); + } finally { + clipboard.dispose(); + } + + // 2. Show notification (must run on UI thread) + display.asyncExec(() -> { + NotificationPopup popup = new NotificationPopup(display, notificationTitle, + notificationMessage); + popup.open(); + }); + return true; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error copying to clipboard: " + e.getMessage(), e); + return false; + } + } +} + diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/EmojiUnicodes.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/EmojiUnicodes.java new file mode 100644 index 00000000..14296ced --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/EmojiUnicodes.java @@ -0,0 +1,76 @@ +package com.checkmarx.eclipse.devassist.utils; + +/** + * The EmojiUnicodes class provides a set of Unicode constants that represent various commonly + * used emoji symbols. These symbols can be used for UI elements, logging, messages, and other + * text-based functionality where emoji representations are needed. + *

+ * This is a utility class and is not meant to be instantiated. + *

+ * All emoji constants are defined as public static final fields and are immutable. + */ +public final class EmojiUnicodes { + + private EmojiUnicodes() { + } + + // ✅ Green check mark + public static final String CHECK = "\u2705"; + + // ❌ Red cross mark + public static final String CROSS = "\u274C"; + + // 🔒 Lock + public static final String LOCK = "\uD83D\uDD12"; + + // 🔁 Repeat Button + public static final String REPEAT = "\uD83D\uDD01"; + + // ⚠️ Warning Sign + public static final String WARNING = "\u26A0\uFE0F"; + + // 🐳 Whale + public static final String WHALE = "\uD83D\uDC33"; + + // ℹ️️ Information Source + public static final String INFO = "\u2139\uFE0F"; + + // ❗ Red Exclamation Mark + public static final String EXCLAMATION = "\u2757"; + + // 👉 Backhand Index Pointing Right + public static final String POINT_RIGHT = "\uD83D\uDC49"; + + // 🔍 Magnifying Glass Tilted Left + public static final String SEARCH = "\uD83D\uDD0D"; + + // 🧠 Brain + public static final String BRAIN = "\uD83E\uDDE0"; + + // 📋 Clipboard + public static final String CLIPBOARD = "\uD83D\uDCCB"; + + // ✏️ Pencil (emoji-style includes variation selector) + public static final String PENCIL = "\u270F\uFE0F"; + + // 🛠️ Hammer and Wrench + public static final String TOOLS = "\uD83D\uDEE0\uFE0F"; + + // 🧨 Firecracker + public static final String FIRECRACKER = "\uD83E\uDDE8"; + + // 🚨 Police Light + public static final String POLICE_LIGHT = "\uD83D\uDEA8"; + + // 🏗️ Building Construction + public static final String CONSTRUCTION = "\uD83C\uDFD7\uFE0F"; + + // 📖 Open Book + public static final String OPEN_BOOK = "\uD83D\uDCD6"; + + // 🛡️ Shield (emoji-style includes variation selector) + public static final String SHIELD = "\uD83D\uDEE1\uFE0F"; + + // 📚 Books + public static final String BOOKS = "\uD83D\uDCDA"; +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java new file mode 100644 index 00000000..26086213 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java @@ -0,0 +1,21 @@ +package com.checkmarx.eclipse.devassist.utils; + +/** + * Enumeration representing various scanning engines supported by the system. + * Each constant signifies a specific type of scanning capability provided by the platform. + * + * The available scanning engines are: + * - OSS: Represents scanning for Open Source Software dependencies and vulnerabilities. + * - SECRETS: Represents scanning for sensitive information such as secrets and credentials in the code. + * - CONTAINERS: Represents scanning for vulnerabilities in container images. + * - IAC: Represents scanning for Infrastructure as Code issues and misconfigurations. + * - ASCA: Represents scanning for Application Security Code Analysis. + */ +public enum ScanEngine { + OSS, + SECRETS, + CONTAINERS, + IAC, + ASCA, + ALL +} diff --git a/pom.xml b/pom.xml index ddc93a88..11e77754 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,8 @@ + common-lib + devassist-lib checkmarx-ast-eclipse-plugin com.checkmarx.eclipse.feature com.checkmarx.eclipse.site @@ -118,7 +120,7 @@ ${project.artifactId}_${unqualifiedVersion}.${buildQualifier} - + @@ -130,7 +132,7 @@ ${tycho.version} JavaSE-17 - ignore + consider linux