diff --git a/.classpath b/.classpath index ac37fb2e..b660d46e 100644 --- a/.classpath +++ b/.classpath @@ -1,5 +1,9 @@ - + + + + + diff --git a/.gitattributes b/.gitattributes index b9b6fd84..f071b16d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,5 @@ 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 +common-lib/lib/*.jar filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index d644c0cc..3911a90c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ .vs/ *.jar !checkmarx-ast-eclipse-plugin/lib/*.jar +!devassist-lib/lib/*.jar +!common-lib/lib/*.jar /.vscode diff --git a/README.md b/README.md index 48e28655..60777ee8 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,27 @@
  1. Overview -
  2. -
  3. - Getting Started
  4. -
  5. Usage
  6. Contribution
  7. License
  8. Contact
  9. @@ -54,55 +66,82 @@ -## Overview +# Overview + +Checkmarx continues to spearhead the shift-left approach to AppSec by bringing our powerful AppSec tools into your IDE. This empowers developers to identify vulnerabilities and remediate them **as they code**. The Checkmarx One Eclipse plugin integrates seamlessly into your IDE, enabling you to access the full functionality of your Checkmarx One account (SAST, SCA, IaC, and Secret Detection) directly from your IDE. +This plugin contains two separate capabilities: -Checkmarx continues to spearhead the shift-left approach to AppSec by bringing our powerful AppSec tools into your IDE. This empowers developers to identify vulnerabilities and remediate them **as they code**. The Checkmarx Eclipse plugin integrates seamlessly into your IDE, enabling you to access the full functionality of your Checkmarx One account (SAST, SCA, IaC Security) directly from your IDE. +- Checkmarx One Platform -You can run new scans, or import results from scans run in your Checkmarx One account. Checkmarx provides detailed info about each vulnerability, including remediation recommendations and examples of effective remediation. The plugin enables you to navigate from a vulnerability to the relevant source code, so that you can easily zero-in on the problematic code and start working on remediation. +- Checkmarx Developer Assist -### Main Features -* Access the full power of Checkmarx One (SAST, SCA, and IaC Security) directly from your IDE +## Checkmarx One Platform +This tool enables Checkmarx One users to access the full functionality of your Checkmarx One account (SAST, SCA, IaC, and Secret Detection) directly from your IDE. You can run new scans, or import results from scans run in your Checkmarx One account. Checkmarx provides detailed info about each vulnerability, including remediation recommendations and examples of effective remediation. The plugin enables you to navigate from a vulnerability to the relevant source code, so that you can easily zero-in on the problematic code and start working on remediation. + +### Key Features +* Access the full power of Checkmarx One (SAST, SCA, IaC, and Secret Detection) directly from your IDE * Run a new scan from your IDE even before committing the code, or import scan results from your Checkmarx One account -* Provides actionable results including remediation recommendations. Navigate from results panel directly to the highlighted vulnerable code in the editor and get right down to work on the remediation. +* Provides actionable results including remediation recommendations. Navigate from results panel directly to the highlighted vulnerable code in the editor and get right down to work on the remediation. * Group and filter results -* Triage results (by adjusting the severity and state and adding comments) directly from the Visual Studio console +* Triage results (by adjusting the severity and state and adding comments) directly from the Eclipse console (currently supported for SAST and IaC Security) * Links to Codebashing lessons +### Prerequisites - -## Getting Started +- An Eclipse installation, version 2020-09 or above. + > Supported platforms: Windows, Mac, Linux/GTK + +- You have an **API key** for your Checkmarx One account. To create an + API key, see [Generating an API Key](https://checkmarx.com/resource/documents/en/34965-68618-generating-an-api-key.html) + > In order to use this integration for running an end-to-end flow of scanning a project and viewing results with the minimum required permissions, the API Key or user account should have the role `plugin-scanner`. Alternatively, they can have at a minimum the out-of-the-box composite role `ast-scanner` as well as the IAM role `default-roles`. + + +### Initial Setup + +1. Verify that all prerequisites are in place. + +2. Install the **Checkmarx One** plugin and configure the settings as described [here](https://checkmarx.com/resource/documents/en/34965-68729-installing-and-setting-up-the-checkmarx-one-eclipse-plugin.html). + +### Usage + +To see how you can use our tool, please refer to the [Documentation](https://docs.checkmarx.com/en/34965-68731-using-the-checkmarx-one-eclipse-plugin.html). + + +## Checkmarx Developer Assist +Developer Assist is an agentic AI tool that delivers real-time context-aware prevention, remediation, and guidance to developers inside the IDE. + +### Key Features + +* An advanced security agent that delivers real-time context-aware prevention, remediation, and guidance to developers from the IDE.​ +* Real-time scanners identify risks as you code. +* **ASCA**, a lightweight source code scanner, enables developers to identify secure coding best practice violations in the file that they are working on as they code. +* Specialized real-time scanners identify vulnerable open source packages and container images, as well as exposed secrets and IaC risks. +* MCP-based agentic AI remediation. +* AI-powered explanation of risk details. +* Reduce noise by marking false positives as ignored. ### Prerequisites -- An eclipse installation, version 2019-03 (4.11) or above. - > Supported platforms: Windows, Mac, Linux/GTK + - Eclipse installation, version 2025-06 and above with GitHub Copilot + - A Checkmarx One account with a **Checkmarx One Assist** license. Also, **Dev Assist** must be activated for your tenant account in the Checkmarx One UI under **Global Settings > Plugins** page. This must be done by an account admin. +You will need to provide an **API key** for your Checkmarx One account. To create an API key, see [Generating an API Key](https://checkmarx.com/resource/documents/en/34965-68618-generating-an-api-key.html) + -- You have an **API key** for your Checkmarx One account. To create an - API key, see [Generating an API Key](https://checkmarx.com/resource/documents/en/34965-68618-generating-an-api-key.html) -> In order to use this integration for running an end-to-end flow of - scanning a project and viewing results, the API Key must have at a - minimum the out-of-the-box composite role `ast-scanner` as well as - the IAM role `default-roles`. -### Setting Up +### Initial Setup 1. Verify that all prerequisites are in place. 2. Install the **Checkmarx One** plugin and configure the settings as described [here](https://checkmarx.com/resource/documents/en/34965-68729-installing-and-setting-up-the-checkmarx-one-eclipse-plugin.html). +3. After authentication, in the welcome screen, select the checkbox next to "Code Smarter with Checkmarx One Assist​". -## Usage +### Usage -To see how you can use our tool, please refer to the [Documentation](https://checkmarx.com/resource/documents/en/34965-68728-checkmarx-one-eclipse-plugin.html) +To see how you can use our tool, please refer to the [Documentation](https://docs.checkmarx.com/en/34965-68731-using-the-checkmarx-one-eclipse-plugin.html). -## How To Videos -- **Installing and Setting up the Plugin** -![GIF - How to install and set up the plugin](https://checkmarx.com/resource/documents/en/image/164fe2f621388d.gif "GIF - How to install and set up the plugin") +**GIF - AI Remediation with Developer Assist** + -- **Running a Scan from the IDE** -![Running a Scan from the IDE](https://checkmarx.com/resource/documents/en/image/164fe2f62587ef.gif "Running a Scan from the IDE") - -- **Loading and Viewing Scan Results** -[![Loading and Viewing Scan Results](https://checkmarx.com/resource/documents/en/image/164fe2f6237dfa.gif "Loading and Viewing Scan Results")](https://checkmarx.com/resource/documents/en/image/164fe2f6237dfa.gif "Loading and Viewing Scan Results") ## Contribution @@ -123,7 +162,7 @@ Project Link: [https://github.com/Checkmarx/ast-eclipse-plugin](https://github.c Find more integrations from our team [here](https://github.com/Checkmarx/ci-cd-integrations#checkmarx-ast-integrations) -© 2022 Checkmarx Ltd. All Rights Reserved. +© 2026 Checkmarx Ltd. All Rights Reserved. [contributors-shield]: https://img.shields.io/github/contributors/Checkmarx/ast-eclipse-plugin.svg [contributors-url]: https://github.com/Checkmarx/ast-eclipse-plugin/graphs/contributors @@ -137,4 +176,3 @@ Find more integrations from our team [here](https://github.com/Checkmarx/ci-cd-i [license-url]: https://github.com/Checkmarx/ast-eclipse-plugin/blob/main/LICENSE [install-shield]: https://img.shields.io/eclipse-marketplace/dt/checkmarx-ast-plugin [install-url]: https://marketplace.eclipse.org/content/checkmarx-ast-plugin - diff --git a/ast-cli-java-wrapper.version b/ast-cli-java-wrapper.version index 0cb980f1..05cfbc0c 100644 --- a/ast-cli-java-wrapper.version +++ b/ast-cli-java-wrapper.version @@ -1 +1 @@ -2.4.24 +2.4.27 diff --git a/checkmarx-ast-eclipse-plugin-tests/.classpath b/checkmarx-ast-eclipse-plugin-tests/.classpath index 98ee5fe7..f4292d57 100644 --- a/checkmarx-ast-eclipse-plugin-tests/.classpath +++ b/checkmarx-ast-eclipse-plugin-tests/.classpath @@ -1,6 +1,6 @@ - + @@ -14,7 +14,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..0f4cbb8c 100644 --- a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF @@ -14,3 +14,5 @@ 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, + org.slf4j;version="[2.0.0,3.0.0)" diff --git a/checkmarx-ast-eclipse-plugin-tests/pom.xml b/checkmarx-ast-eclipse-plugin-tests/pom.xml index 2d5b4ef1..70e9bd00 100644 --- a/checkmarx-ast-eclipse-plugin-tests/pom.xml +++ b/checkmarx-ast-eclipse-plugin-tests/pom.xml @@ -43,7 +43,6 @@ XML CSV - HTML 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..24dce08b 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 @@ -1,28 +1,24 @@ package checkmarx.ast.eclipse.plugin.tests.integration; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.checkmarx.eclipse.runner.Authenticator; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import com.checkmarx.eclipse.common.runner.Authenticator; + public class AuthenticatorIntegrationTest extends BaseIntegrationTest { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticatorIntegrationTest.class); - - @Mock + private Authenticator authenticator; @Test public void testSuccessfulAuthentication() { - authenticator = new Authenticator(LoggerFactory.getLogger(Authenticator.class)); + authenticator = Authenticator.INSTANCE; System.out.println("\n=== Starting Authentication Test ==="); System.out.println("Current directory: " + new File(".").getAbsolutePath()); System.out.println("API Key available: " + (VALID_API_KEY != null)); @@ -35,7 +31,7 @@ public void testSuccessfulAuthentication() { @Test public void testInvalidApiKeyAuthentication() { - authenticator = new Authenticator(LoggerFactory.getLogger(Authenticator.class)); + authenticator = Authenticator.INSTANCE; System.out.println("\n=== Starting Invalid API Key Test ==="); String invalidApiKey = "invalid-api-key"; String result = authenticator.doAuthentication(invalidApiKey, ""); 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..3e1f0d67 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 { @@ -17,9 +17,6 @@ void testGetSeverity_allConstants() { assertEquals(Severity.MEDIUM, Severity.getSeverity("MEDIUM")); assertEquals(Severity.LOW, Severity.getSeverity("LOW")); assertEquals(Severity.INFO, Severity.getSeverity("INFO")); - assertEquals(Severity.GROUP_BY_SEVERITY, Severity.getSeverity("GROUP_BY_SEVERITY")); - assertEquals(Severity.GROUP_BY_QUERY_NAME, Severity.getSeverity("GROUP_BY_QUERY_NAME")); - assertEquals(Severity.GROUP_BY_STATE_NAME, Severity.getSeverity("GROUP_BY_STATE_NAME")); } @Test @@ -49,9 +46,6 @@ void testValueOf_allConstantsExist() { assertDoesNotThrow(() -> Severity.valueOf("MEDIUM")); assertDoesNotThrow(() -> Severity.valueOf("LOW")); assertDoesNotThrow(() -> Severity.valueOf("INFO")); - assertDoesNotThrow(() -> Severity.valueOf("GROUP_BY_SEVERITY")); - assertDoesNotThrow(() -> Severity.valueOf("GROUP_BY_QUERY_NAME")); - assertDoesNotThrow(() -> Severity.valueOf("GROUP_BY_STATE_NAME")); } @Test @@ -71,9 +65,6 @@ void testEnumName_correctValues() { assertEquals("MEDIUM", Severity.MEDIUM.name()); assertEquals("LOW", Severity.LOW.name()); assertEquals("INFO", Severity.INFO.name()); - assertEquals("GROUP_BY_SEVERITY", Severity.GROUP_BY_SEVERITY.name()); - assertEquals("GROUP_BY_QUERY_NAME", Severity.GROUP_BY_QUERY_NAME.name()); - assertEquals("GROUP_BY_STATE_NAME", Severity.GROUP_BY_STATE_NAME.name()); } @Test @@ -83,9 +74,6 @@ void testEnumOrdinal_correctSequence() { assertEquals(2, Severity.MEDIUM.ordinal()); assertEquals(3, Severity.LOW.ordinal()); assertEquals(4, Severity.INFO.ordinal()); - assertEquals(5, Severity.GROUP_BY_SEVERITY.ordinal()); - assertEquals(6, Severity.GROUP_BY_QUERY_NAME.ordinal()); - assertEquals(7, Severity.GROUP_BY_STATE_NAME.ordinal()); } @Test @@ -106,7 +94,6 @@ void testValues_containsAllExpected() { if (s == Severity.CRITICAL) hasCritical = true; if (s == Severity.HIGH) hasHigh = true; if (s == Severity.LOW) hasLow = true; - if (s == Severity.GROUP_BY_SEVERITY) hasGroupBy = true; } assertTrue(hasCritical); @@ -142,7 +129,6 @@ void testSeverityComparison_differentInstancesNotEqual() { void testToString_returnsName() { assertEquals("CRITICAL", Severity.CRITICAL.toString()); assertEquals("HIGH", Severity.HIGH.toString()); - assertEquals("GROUP_BY_SEVERITY", Severity.GROUP_BY_SEVERITY.toString()); } @Test @@ -210,9 +196,6 @@ void testEnumConstants_arePublicStaticFinal() { @Test void testGroupByConstants_arePublicStaticFinal() { - assertNotNull(Severity.GROUP_BY_SEVERITY); - assertNotNull(Severity.GROUP_BY_QUERY_NAME); - assertNotNull(Severity.GROUP_BY_STATE_NAME); } @Test @@ -233,9 +216,6 @@ void testGetSeverity_multipleCallsSequence() { @Test void testGetSeverity_allGroupByVariants() { - assertEquals(Severity.GROUP_BY_SEVERITY, Severity.getSeverity("GROUP_BY_SEVERITY")); - assertEquals(Severity.GROUP_BY_QUERY_NAME, Severity.getSeverity("GROUP_BY_QUERY_NAME")); - assertEquals(Severity.GROUP_BY_STATE_NAME, Severity.getSeverity("GROUP_BY_STATE_NAME")); } @ParameterizedTest @@ -247,7 +227,6 @@ void testGetSeverity_validStandardSeverities(String severityName) { } @ParameterizedTest - @ValueSource(strings = {"GROUP_BY_SEVERITY", "GROUP_BY_QUERY_NAME", "GROUP_BY_STATE_NAME"}) void testGetSeverity_validGroupingOptions(String groupingName) { Severity severity = Severity.getSeverity(groupingName); assertNotNull(severity); 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..52126ba1 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 { @@ -33,21 +33,6 @@ void testGetSeverity_info() { assertEquals(Severity.INFO, Severity.getSeverity("INFO")); } - @Test - void testGetSeverity_groupBySeverity() { - assertEquals(Severity.GROUP_BY_SEVERITY, Severity.getSeverity("GROUP_BY_SEVERITY")); - } - - @Test - void testGetSeverity_groupByQueryName() { - assertEquals(Severity.GROUP_BY_QUERY_NAME, Severity.getSeverity("GROUP_BY_QUERY_NAME")); - } - - @Test - void testGetSeverity_groupByStateName() { - assertEquals(Severity.GROUP_BY_STATE_NAME, Severity.getSeverity("GROUP_BY_STATE_NAME")); - } - @Test void testGetSeverity_unknownValue_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> Severity.getSeverity("UNKNOWN_SEVERITY_XYZ")); 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..90710152 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 @@ -1,7 +1,10 @@ package checkmarx.ast.eclipse.plugin.tests.unit.runner; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; import java.io.IOException; @@ -9,29 +12,25 @@ import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.slf4j.Logger; 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 { @Test void testDoAuthenticationSuccess() throws Exception { - Logger mockLogger = mock(Logger.class); try (MockedConstruction mocked = Mockito.mockConstruction(CxWrapper.class, (mock, context) -> when(mock.authValidate()).thenReturn("SUCCESS")); MockedStatic mockedCxLogger = Mockito.mockStatic(CxLogger.class)) { - Authenticator authenticator = new Authenticator(mockLogger); - - String result = authenticator.doAuthentication("dummyKey", "--param"); + String result = Authenticator.INSTANCE.doAuthentication("dummyKey", "--param"); assertEquals("SUCCESS", result); mockedCxLogger.verify(() -> CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, "SUCCESS"))); @@ -41,7 +40,6 @@ void testDoAuthenticationSuccess() throws Exception { @Test void testDoAuthenticationIOException() throws Exception { - Logger mockLogger = mock(Logger.class); try (MockedConstruction mocked = Mockito.mockConstruction(CxWrapper.class, @@ -49,9 +47,7 @@ void testDoAuthenticationIOException() throws Exception { .thenThrow(new IOException("IO error"))); MockedStatic mockedCxLogger = Mockito.mockStatic(CxLogger.class)) { - Authenticator authenticator = new Authenticator(mockLogger); - - String result = authenticator.doAuthentication("dummyKey", "--param"); + String result = Authenticator.INSTANCE.doAuthentication("dummyKey", "--param"); assertEquals("IO error", result); mockedCxLogger.verify(() -> CxLogger.error( @@ -64,7 +60,6 @@ void testDoAuthenticationIOException() throws Exception { @Test void testDoAuthenticationInterruptedException() throws Exception { - Logger mockLogger = mock(Logger.class); try (MockedConstruction mocked = Mockito.mockConstruction(CxWrapper.class, @@ -72,9 +67,7 @@ void testDoAuthenticationInterruptedException() throws Exception { .thenThrow(new InterruptedException("Interrupted"))); MockedStatic mockedCxLogger = Mockito.mockStatic(CxLogger.class)) { - Authenticator authenticator = new Authenticator(mockLogger); - - String result = authenticator.doAuthentication("dummyKey", "--param"); + String result = Authenticator.INSTANCE.doAuthentication("dummyKey", "--param"); assertEquals("Interrupted", result); mockedCxLogger.verify(() -> CxLogger.error( @@ -87,17 +80,13 @@ void testDoAuthenticationInterruptedException() throws Exception { @Test void testDoAuthenticationCxException() throws Exception { - Logger mockLogger = mock(Logger.class); - try (MockedConstruction mocked = Mockito.mockConstruction(CxWrapper.class, (mock, context) -> when(mock.authValidate()) .thenThrow(new CxException(1, "Cx error"))); MockedStatic mockedCxLogger = Mockito.mockStatic(CxLogger.class)) { - Authenticator authenticator = new Authenticator(mockLogger); - - String result = authenticator.doAuthentication("dummyKey", "--param"); + String result = Authenticator.INSTANCE.doAuthentication("dummyKey", "--param"); assertEquals("Cx error", result); mockedCxLogger.verify(() -> CxLogger.error( 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..ac38179d 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; @@ -167,7 +167,7 @@ void testAreCredentialsDefinedTrue() { try (MockedStatic prefs = Mockito.mockStatic(Preferences.class)) { - prefs.when(Preferences::getApiKey).thenReturn("apikey"); + prefs.when(Preferences::isAuthenticated).thenReturn(true); boolean result = PluginUtils.areCredentialsDefined(); @@ -180,7 +180,7 @@ void testAreCredentialsDefinedFalse() { try (MockedStatic prefs = Mockito.mockStatic(Preferences.class)) { - prefs.when(Preferences::getApiKey).thenReturn(""); + prefs.when(Preferences::isAuthenticated).thenReturn(false); boolean result = PluginUtils.areCredentialsDefined(); 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..af8d8612 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; @@ -135,10 +135,8 @@ void testRefreshToolbarRecreatesActions() { @Test void testGroupBySeverityAction() { - // The GROUP_BY_SEVERITY action calls FilterState.setState(Severity.GROUP_BY_SEVERITY). // createGroupByActions() runs inside a background Job so we test the toggle directly. FilterState.groupBySeverity = false; - FilterState.setState(Severity.GROUP_BY_SEVERITY); assertTrue(FilterState.groupBySeverity); } @@ -186,17 +184,14 @@ void testStaticConstant_menuGroupBy() { @Test void testStaticConstant_groupBySeverity() { - assertEquals("Severity", ToolBarActions.GROUP_BY_SEVERITY); } @Test void testStaticConstant_groupByQueryName() { - assertEquals("Query Name", ToolBarActions.GROUP_BY_QUERY_NAME); } @Test void testStaticConstant_groupByStateName() { - assertEquals("State Name", ToolBarActions.GROUP_BY_STATE_NAME); } @Test 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..089d188b 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; @@ -87,7 +87,6 @@ void testSetState_groupBySeverityToggle() { boolean beforeState = FilterState.groupBySeverity; try (MockedStatic mockSettings = mockStatic(GlobalSettings.class)) { - FilterState.setState(Severity.GROUP_BY_SEVERITY); assertNotEquals(beforeState, FilterState.groupBySeverity); } } 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..ad5da5c0 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; @@ -69,25 +69,19 @@ void testIsSeverityEnabled_info() { @Test void testIsSeverityEnabled_groupBySeverity() { FilterState.groupBySeverity = true; - assertTrue(FilterState.isSeverityEnabled("GROUP_BY_SEVERITY")); FilterState.groupBySeverity = false; - assertFalse(FilterState.isSeverityEnabled("GROUP_BY_SEVERITY")); } @Test void testIsSeverityEnabled_groupByQueryName() { FilterState.groupByQueryName = false; - assertFalse(FilterState.isSeverityEnabled("GROUP_BY_QUERY_NAME")); FilterState.groupByQueryName = true; - assertTrue(FilterState.isSeverityEnabled("GROUP_BY_QUERY_NAME")); } @Test void testIsSeverityEnabled_groupByStateName() { FilterState.groupByStateName = false; - assertFalse(FilterState.isSeverityEnabled("GROUP_BY_STATE_NAME")); FilterState.groupByStateName = true; - assertTrue(FilterState.isSeverityEnabled("GROUP_BY_STATE_NAME")); } // ─── setState ──────────────────────────────────────────────────────────── @@ -142,7 +136,6 @@ void testSetState_info_togglesFromFalse() { void testSetState_groupBySeverity_toggles() { FilterState.groupBySeverity = true; try (MockedStatic gs = Mockito.mockStatic(GlobalSettings.class)) { - FilterState.setState(Severity.GROUP_BY_SEVERITY); } assertFalse(FilterState.groupBySeverity); } @@ -151,7 +144,6 @@ void testSetState_groupBySeverity_toggles() { void testSetState_groupByQueryName_toggles() { FilterState.groupByQueryName = false; try (MockedStatic gs = Mockito.mockStatic(GlobalSettings.class)) { - FilterState.setState(Severity.GROUP_BY_QUERY_NAME); } assertTrue(FilterState.groupByQueryName); } @@ -160,7 +152,6 @@ void testSetState_groupByQueryName_toggles() { void testSetState_groupByStateName_toggles() { FilterState.groupByStateName = false; try (MockedStatic gs = Mockito.mockStatic(GlobalSettings.class)) { - FilterState.setState(Severity.GROUP_BY_STATE_NAME); } assertTrue(FilterState.groupByStateName); } @@ -466,9 +457,6 @@ void testGetFilterStateListForPanel_containsPredefinedStates() { // gs.when(() -> GlobalSettings.getFromPreferences("MEDIUM", "true")).thenReturn("true"); // gs.when(() -> GlobalSettings.getFromPreferences("LOW", "false")).thenReturn("true"); // gs.when(() -> GlobalSettings.getFromPreferences("INFO", "false")).thenReturn("false"); -// gs.when(() -> GlobalSettings.getFromPreferences("GROUP_BY_SEVERITY", "true")).thenReturn("true"); -// gs.when(() -> GlobalSettings.getFromPreferences("GROUP_BY_QUERY_NAME", "false")).thenReturn("false"); -// gs.when(() -> GlobalSettings.getFromPreferences("GROUP_BY_STATE_NAME", "false")).thenReturn("false"); // gs.when(() -> GlobalSettings.getFromPreferences("NOT_EXPLOITABLE", "false")).thenReturn("true"); // gs.when(() -> GlobalSettings.getFromPreferences("CONFIRMED", "true")).thenReturn("true"); // gs.when(() -> GlobalSettings.getFromPreferences("TO_VERIFY", "true")).thenReturn("true"); diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java new file mode 100644 index 00000000..5fcd2be6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java @@ -0,0 +1,62 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.wrapper; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.wrapper.CxWrapperFactory; + +class CxWrapperFactoryTest { + + @Test + void testBuildWithNoArgs_usesSavedPreferencesAndStampsAgentName() throws Exception { + AtomicReference capturedConfig = new AtomicReference<>(); + + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> capturedConfig.set((CxConfig) context.arguments().get(0))); + var mockedPreferences = mockStatic(Preferences.class)) { + + mockedPreferences.when(Preferences::getApiKey).thenReturn("saved-api-key"); + mockedPreferences.when(Preferences::getAdditionalOptions).thenReturn("--saved-param"); + + CxWrapperFactory.build(); + + assertEquals(1, mocked.constructed().size()); + CxConfig config = capturedConfig.get(); + assertNotNull(config); + assertEquals("saved-api-key", config.getApiKey()); + assertEquals("--saved-param", String.join(" ", config.getAdditionalParameters())); + assertNotNull(config.getAgentName()); + assertTrue(config.getAgentName().startsWith("Eclipse_"), + "Agent name should be stamped as Eclipse_, was: " + config.getAgentName()); + } + } + + @Test + void testBuildWithExplicitCredentials_doesNotUseSavedPreferences() throws Exception { + AtomicReference capturedConfig = new AtomicReference<>(); + + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> capturedConfig.set((CxConfig) context.arguments().get(0))); + var mockedPreferences = mockStatic(Preferences.class)) { + + CxWrapperFactory.build("typed-api-key", "--typed-param"); + + assertEquals(1, mocked.constructed().size()); + CxConfig config = capturedConfig.get(); + assertNotNull(config); + assertEquals("typed-api-key", config.getApiKey()); + assertEquals("--typed-param", String.join(" ", config.getAdditionalParameters())); + assertTrue(config.getAgentName().startsWith("Eclipse_")); + + mockedPreferences.verifyNoInteractions(); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java new file mode 100644 index 00000000..263f1ab9 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java @@ -0,0 +1,63 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.wrapper; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; + +import com.checkmarx.ast.project.Project; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; + +class WrapperProviderTest { + + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + @Test + void testIsAiMcpServerEnabled_forwardsCredentialsAndReturnsWrapperResult() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.aiMcpServerEnabled()).thenReturn(true))) { + + boolean result = wrapperProvider.isAiMcpServerEnabled("api-key", "--param"); + + assertTrue(result); + assertEquals(1, mocked.constructed().size()); + } + } + + @Test + void testIsAiMcpServerEnabled_propagatesFalseWhenDisabled() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.aiMcpServerEnabled()).thenReturn(false))) { + + boolean result = wrapperProvider.isAiMcpServerEnabled("api-key", "--param"); + + assertFalse(result); + } + } + + @Test + void testGetProjects_forwardsLimitAndReturnsWrapperResult() throws Exception { + Project mockProject = mock(Project.class); + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.projectList("limit=10")).thenReturn(List.of(mockProject)))) { + + List projects = wrapperProvider.getProjects("limit=10"); + + assertEquals(1, projects.size()); + assertSame(mockProject, projects.get(0)); + } + } + + @Test + void testTriageGetStates_propagatesExceptionFromWrapper() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.triageGetStates(false)).thenThrow(new RuntimeException("boom")))) { + + assertThrows(RuntimeException.class, () -> wrapperProvider.triageGetStates(false)); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/.classpath b/checkmarx-ast-eclipse-plugin/.classpath index 32e2245e..6ed62e61 100644 --- a/checkmarx-ast-eclipse-plugin/.classpath +++ b/checkmarx-ast-eclipse-plugin/.classpath @@ -7,25 +7,22 @@ - - - - - - - - - - - - - + + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF index f9af0477..c374c236 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -5,31 +5,31 @@ 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.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 diff --git a/checkmarx-ast-eclipse-plugin/build.properties b/checkmarx-ast-eclipse-plugin/build.properties index 75590c4f..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.4.jar,\ - lib/commons-lang3-3.18.0.jar,\ - lib/ast-cli-java-wrapper-2.4.24.jar,\ lib/org.eclipse.mylyn.commons.ui_4.9.0.v20251121-0615.jar,\ - lib/jackson-databind-2.21.5.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/star-action.svg b/checkmarx-ast-eclipse-plugin/icons/severity/star-action.svg new file mode 100644 index 00000000..bfc23248 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/star-action.svg @@ -0,0 +1,9 @@ + + + + + + + + + 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..c3a8e6e3 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/enums/GroupingMode.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/enums/GroupingMode.java new file mode 100644 index 00000000..7dd44d7a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/enums/GroupingMode.java @@ -0,0 +1,34 @@ +package com.checkmarx.eclipse.enums; + +/** + * UI grouping modes for the CheckmarxView findings tree. + * Separate from Severity enum to keep shared severity contract clean. + * + * Determines how findings are organized/grouped in the findings tree view. + */ +public enum GroupingMode { + SEVERITY("Group by Severity"), + QUERY_NAME("Group by Query Name"), + STATE_NAME("Group by State"); + + private final String displayName; + + GroupingMode(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public static GroupingMode fromString(String value) { + if (value == null) { + return SEVERITY; // Default + } + try { + return GroupingMode.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return SEVERITY; // Default for invalid values + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/enums/Severity.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/enums/Severity.java deleted file mode 100644 index 3cb436d8..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/enums/Severity.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.checkmarx.eclipse.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/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/runner/Authenticator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/runner/Authenticator.java deleted file mode 100644 index 202baa99..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/runner/Authenticator.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.checkmarx.eclipse.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.utils.CxLogger; -import com.checkmarx.eclipse.utils.PluginConstants; - -public class Authenticator { - private final Logger log; - - private Authenticator() { - this.log = LoggerFactory.getLogger(Authenticator.class); - } - - // for test only - public Authenticator(Logger logger) { - this.log = logger; - } - - protected static final String AUTH_STATUS = "Authentication Status: "; - public static final Authenticator INSTANCE = new Authenticator(); - - public String doAuthentication(String apiKey, String additionalParams) { - CxConfig config = CxConfig.builder() - .apiKey(apiKey) - .additionalParameters(additionalParams) - .build(); - try { - CxWrapper wrapper = new CxWrapper(config, log); - String cxValidateOutput = wrapper.authValidate(); - CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, cxValidateOutput)); - return cxValidateOutput; - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, e.getMessage()), e); - return e.getMessage(); - } - } - } \ 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..74589955 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 @@ -20,12 +20,13 @@ import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.ui.PlatformUI; -import org.apache.commons.lang3.StringUtils; 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; @@ -49,10 +50,11 @@ public static String convertStringTimeStamp(String timestamp) { Instant instant = Instant.parse(timestamp); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN).withZone(ZoneId.systemDefault()); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN) + .withZone(ZoneId.systemDefault()); parsedDate = dateTimeFormatter.format(instant); } catch (Exception e) { - System.out.println(e); + CxLogger.warning(String.format("[TIME-STAMP] Returning time stamp", e.getMessage())); return timestamp; } @@ -97,19 +99,21 @@ public static void setTextForComboViewer(ComboViewer comboViewer, String text) { */ public static void updateFiltersEnabledAndCheckedState(List filterActions) { for (Action action : filterActions) { - // avoid to disable group by severity , group by query name and group by state actions - if (!action.getId().equals(ActionName.GROUP_BY_SEVERITY.name()) && !action.getId().equals(ActionName.GROUP_BY_QUERY_NAME.name()) && !action.getId().equals(ActionName.GROUP_BY_STATE_NAME.name()) ) { + // avoid to disable group by severity , group by query name and group by state + // actions + if (!action.getId().equals(ActionName.GROUP_BY_SEVERITY.name()) + && !action.getId().equals(ActionName.GROUP_BY_QUERY_NAME.name()) + && !action.getId().equals(ActionName.GROUP_BY_STATE_NAME.name())) { action.setEnabled(DataProvider.getInstance().containsResults()); } - - if(!action.getId().equals(ActionName.FILTER_CHANGED.name())) { + + if (!action.getId().equals(ActionName.FILTER_CHANGED.name())) { action.setChecked(FilterState.isSeverityEnabled(action.getId())); } - } } - + /** * Create a display model to be presented in the tree * @@ -119,7 +123,7 @@ public static void updateFiltersEnabledAndCheckedState(List filterAction public static DisplayModel message(String message) { return new DisplayModel.DisplayModelBuilder(message).build(); } - + /** * Show message in the tree * @@ -130,17 +134,16 @@ public static void showMessage(DisplayModel rootModel, TreeViewer viewer, String rootModel.children.add(PluginUtils.message(message)); viewer.refresh(); } - - + /** * Clear message in the tree * */ - public static void clearMessage(DisplayModel rootModel,TreeViewer viewer) { + public static void clearMessage(DisplayModel rootModel, TreeViewer viewer) { rootModel.children.clear(); viewer.refresh(); } - + /** * Get Event Broker * @@ -149,16 +152,16 @@ public static void clearMessage(DisplayModel rootModel,TreeViewer viewer) { public static IEventBroker getEventBroker() { return (IEventBroker) PlatformUI.getWorkbench().getService(IEventBroker.class); } - + /** - * Check if checkmarx credentials are defined in the Preferences - * + * Check if the user is currently authenticated to Checkmarx One. + * * @return */ public static boolean areCredentialsDefined() { - return StringUtils.isNotBlank(Preferences.getApiKey()); + return Preferences.isAuthenticated(); } - + /** * Add Checkmarx vulnerabilities to Problems View * @@ -192,7 +195,7 @@ public static void addVulnerabilitiesToProblemsView(List resultsList) { } } } - + /** * Get IMarker severity based on each checkmarx result severity * @@ -201,25 +204,25 @@ public static void addVulnerabilitiesToProblemsView(List resultsList) { */ private static Integer getIMarkerSeverity(String resultSeverity) { Severity severity = Severity.getSeverity(resultSeverity); - + switch (severity) { - case CRITICAL: - return IMarker.SEVERITY_ERROR; - case HIGH: - return IMarker.SEVERITY_ERROR; - case MEDIUM: - return IMarker.SEVERITY_WARNING; - case LOW: - return IMarker.SEVERITY_INFO; - case INFO: - return IMarker.SEVERITY_INFO; - default: - break; + case CRITICAL: + return IMarker.SEVERITY_ERROR; + case HIGH: + return IMarker.SEVERITY_ERROR; + case MEDIUM: + return IMarker.SEVERITY_WARNING; + case LOW: + return IMarker.SEVERITY_INFO; + case INFO: + return IMarker.SEVERITY_INFO; + default: + break; } - + return IMarker.SEVERITY_INFO; } - + /** * Find files in workspace * @@ -229,7 +232,8 @@ private static Integer getIMarkerSeverity(String resultSeverity) { public static List findFileInWorkspace(final String fileName) { final List foundFiles = new ArrayList(); try { - // visiting only resources proxy because we obtain the resource only when matching name, thus the workspace traversal is much faster + // visiting only resources proxy because we obtain the resource only when + // matching name, thus the workspace traversal is much faster ResourcesPlugin.getWorkspace().getRoot().accept(new IResourceProxyVisitor() { @Override public boolean visit(IResourceProxy resourceProxy) throws CoreException { @@ -248,7 +252,7 @@ public boolean visit(IResourceProxy resourceProxy) throws CoreException { } return foundFiles; } - + /** * Clear checkmarx vulnerabilities from Problems View */ @@ -256,15 +260,16 @@ public static void clearVulnerabilitiesFromProblemsView() { IWorkspace workspace = ResourcesPlugin.getWorkspace(); IResource resource = workspace.getRoot(); IMarker[] markers; - + try { markers = resource.findMarkers(IMarker.MARKER, true, IResource.DEPTH_INFINITE); - + for (IMarker m : markers) { - if(m.getAttribute(IMarker.SOURCE_ID) != null && m.getAttribute(IMarker.SOURCE_ID).equals(PluginConstants.PROBLEM_SOURCE_ID)) { + if (m.getAttribute(IMarker.SOURCE_ID) != null + && m.getAttribute(IMarker.SOURCE_ID).equals(PluginConstants.PROBLEM_SOURCE_ID)) { m.delete(); } - } + } } catch (CoreException e) { CxLogger.error(String.format(PluginConstants.ERROR_FINDING_OR_DELETING_MARKER, e.getMessage()), e); } 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..af4d89a2 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,19 @@ 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.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.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; @@ -121,12 +127,12 @@ public class CheckmarxView extends ViewPart implements EventHandler { private static final String FORMATTED_SCAN_LABEL_LATEST = "%s %s (%s)"; private boolean isUpdatingCombo = false; private boolean resetStoredProjects = false; - + private Timer debounceTimer = new Timer("ProjectSearchDebounce", true); private TimerTask pendingSearchTask; private static final int DEBOUNCE_DELAY_MS = 400; private volatile String latestProjectSearchTerm = ""; - + private static final int SCROLL_WIDTH = 30; /** * The ID of the view as specified by the extension. @@ -159,7 +165,8 @@ public class CheckmarxView extends ViewPart implements EventHandler { public static final Image BFL = Activator.getImageDescriptor("/icons/checkmarx-plugin-13_dark.png").createImage(); private TreeViewer resultsTree; - private ComboViewer scanIdComboViewer, projectComboViewer, branchComboViewer, triageSeverityComboViewew, triageStateComboViewer; + private ComboViewer scanIdComboViewer, projectComboViewer, branchComboViewer, triageSeverityComboViewew, + triageStateComboViewer; private ISelectionChangedListener triageSeverityComboViewerListener, triageStateComboViewerListener; private Text commentText; private DisplayModel rootModel; @@ -185,8 +192,8 @@ public class CheckmarxView extends ViewPart implements EventHandler { private Link codeBashingLinkText; private CLabel attackVectorLabel; - //private CLabel bflLabel; - //private Text bflText; + // private CLabel bflLabel; + // private Text bflText; private Label attackVectorSeparator; private ToolBarActions toolBarActions; @@ -203,7 +210,6 @@ public class CheckmarxView extends ViewPart implements EventHandler { private List currentProjects = new ArrayList<>(); private List storeCurrentProjects = new ArrayList<>(); - private boolean scansCleanedByProject = false; private boolean firstTimeTriggered = false; @@ -223,7 +229,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,7 +255,12 @@ public void dispose() { public void createPartControl(Composite parent) { this.parent = parent; - // Clear vulnerabilities from Problems View + // Clear any stale vulnerability markers from a previous session before drawing + // the view. + // Markers persist as real Eclipse IMarkers across restarts, so without this + // cleanup, + // vulnerabilities from prior scans can appear until the user changes + // project/branch/scan. PluginUtils.clearVulnerabilitiesFromProblemsView(); if (PluginUtils.areCredentialsDefined()) { @@ -275,11 +286,11 @@ public void onRefsChanged(RefsChangedEvent arg) { } String gitBranch = arg.getRepository().getBranch(); - - if(gitBranch.equals(currentBranch)) { + + if (gitBranch.equals(currentBranch)) { return; } - + updatePluginBranchAndScans(gitBranch); } catch (IOException e) { CxLogger.error(PluginConstants.ERROR_GETTING_GIT_BRANCH, e); @@ -319,7 +330,8 @@ private void createToolbar() { pluginEventBus.register(this); toolBarActions = new ToolBarActions.ToolBarActionsBuilder().actionBars(actionBars).rootModel(rootModel) - .resultsTree(resultsTree).pluginEventBus(pluginEventBus).projectsCombo(projectComboViewer).branchesCombo(branchComboViewer).scansCombo(scanIdComboViewer).build(); + .resultsTree(resultsTree).pluginEventBus(pluginEventBus).projectsCombo(projectComboViewer) + .branchesCombo(branchComboViewer).scansCombo(scanIdComboViewer).build(); } @Override @@ -333,10 +345,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(); @@ -355,7 +370,7 @@ private void drawPluginPanel() { // Create plugin toolBar createToolbar(); - + loadComboboxes(); // Init git branch listener @@ -403,7 +418,8 @@ protected IStatus run(IProgressMonitor arg0) { if (currentProjectId.isEmpty() || currentProjects.isEmpty()) { PluginUtils.setTextForComboViewer(projectComboViewer, PROJECT_COMBO_VIEWER_TEXT); PluginUtils.setTextForComboViewer(branchComboViewer, BRANCH_COMBO_VIEWER_TEXT); - PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); + PluginUtils.setTextForComboViewer(scanIdComboViewer, + PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); PluginUtils.enableComboViewer(projectComboViewer, true); PluginUtils.enableComboViewer(scanIdComboViewer, true); PluginUtils.enableComboViewer(branchComboViewer, false); @@ -424,10 +440,10 @@ protected IStatus run(IProgressMonitor arg0) { PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); }); - + if (!currentBranch.isEmpty()) { - updateStartScanButton(true); sync.asyncExec(() -> { + updateStartScanButton(true); PluginUtils.setTextForComboViewer(branchComboViewer, currentBranch); }); List scanList = DataProvider.getInstance().getScansForProject(currentBranch); @@ -440,7 +456,7 @@ protected IStatus run(IProgressMonitor arg0) { }); if (!currentScanId.isEmpty()) { - String currentScanName = getScanNameFromId(scanList, currentScanId); + String currentScanName = getScanNameFromId(scanList, currentScanId); currentScanIdFormmated = currentScanName; sync.asyncExec(() -> { PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanName); @@ -697,7 +713,7 @@ public void handleEvent(Event e) { * * @param resultsComposite */ - private void createResultVulnerabilitiesPanel(Composite resultsComposite) { + private void createResultVulnerabilitiesPanel(Composite resultsComposite) { attackVectorCompositePanel = new Composite(resultsComposite, SWT.BORDER); attackVectorCompositePanel.setLayout(new FillLayout()); attackVectorCompositePanel.setVisible(false); @@ -715,45 +731,54 @@ private void drawAttackVectorSeparator(Composite parent) { * draw BFL composite */ - /*private void drawBFLComposite() { - bflComposite = new Composite(attackVectorContentComposite, SWT.NONE); - GridLayout bflCompositeLayout = new GridLayout(2, false); - bflComposite.setLayout(bflCompositeLayout); - bflComposite.setSize(attackVectorContentComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - GridData gd_blfComposite = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1); - bflComposite.setLayoutData(gd_blfComposite); - - bflComposite.setBackground(attackVectorContentComposite.getBackground()); - - bflLabel = new CLabel(bflComposite, SWT.HORIZONTAL); - GridData gd_emptyLabel = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1); - gd_emptyLabel.heightHint = PluginConstants.BFL_LABEL_HEIGHT; - bflLabel.setLayoutData(gd_emptyLabel); - bflLabel.setBackground(bflComposite.getBackground()); - - bflText = new Text(bflComposite, SWT.WRAP | SWT.MULTI); - GridData gd_bflText = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); - gd_bflText.widthHint = PluginConstants.BFL_TEXT_MAX_WIDTH; - bflText.setLayoutData(gd_bflText); - bflText.setBackground(bflComposite.getBackground()); - bflText.setData(PluginConstants.DATA_ID_KEY, PluginConstants.BEST_FIX_LOCATION); - }*/ + /* + * private void drawBFLComposite() { + * bflComposite = new Composite(attackVectorContentComposite, SWT.NONE); + * GridLayout bflCompositeLayout = new GridLayout(2, false); + * bflComposite.setLayout(bflCompositeLayout); + * bflComposite.setSize(attackVectorContentComposite.computeSize(SWT.DEFAULT, + * SWT.DEFAULT)); + * GridData gd_blfComposite = new GridData(SWT.FILL, SWT.FILL, false, false, 1, + * 1); + * bflComposite.setLayoutData(gd_blfComposite); + * + * bflComposite.setBackground(attackVectorContentComposite.getBackground()); + * + * bflLabel = new CLabel(bflComposite, SWT.HORIZONTAL); + * GridData gd_emptyLabel = new GridData(SWT.FILL, SWT.FILL, false, false, 1, + * 1); + * gd_emptyLabel.heightHint = PluginConstants.BFL_LABEL_HEIGHT; + * bflLabel.setLayoutData(gd_emptyLabel); + * bflLabel.setBackground(bflComposite.getBackground()); + * + * bflText = new Text(bflComposite, SWT.WRAP | SWT.MULTI); + * GridData gd_bflText = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); + * gd_bflText.widthHint = PluginConstants.BFL_TEXT_MAX_WIDTH; + * bflText.setLayoutData(gd_bflText); + * bflText.setBackground(bflComposite.getBackground()); + * bflText.setData(PluginConstants.DATA_ID_KEY, + * PluginConstants.BEST_FIX_LOCATION); + * } + */ /** * Draw panel when Checkmarx credentials are not defined */ private void drawMissingCredentialsPanel() { - - // Dispose all children to remove any previous panels - for (Control child : parent.getChildren()) { - child.dispose(); - } - openSettingsComposite = new Composite(parent, SWT.NONE); + // Dispose all children to remove any previous panels (plugin panel, etc.) + for (Control child : parent.getChildren()) { + child.dispose(); + } + + // 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)); + openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); final Label hidden = new Label(openSettingsComposite, SWT.NONE); hidden.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); @@ -801,8 +826,7 @@ public String getText(Object element) { return super.getText(element); } }); - - + projectComboViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { @@ -834,7 +858,8 @@ protected IStatus run(IProgressMonitor arg0) { currentBranches = DataProvider.getInstance().getBranchesForProject(selectedProject.getId()); sync.asyncExec(() -> { branchComboViewer.setInput(currentBranches); - PluginUtils.setTextForComboViewer(branchComboViewer, currentBranches.isEmpty() ? NO_BRANCHES_AVAILABLE : BRANCH_COMBO_VIEWER_TEXT); + PluginUtils.setTextForComboViewer(branchComboViewer, + currentBranches.isEmpty() ? NO_BRANCHES_AVAILABLE : BRANCH_COMBO_VIEWER_TEXT); PluginUtils.enableComboViewer(branchComboViewer, true); PluginUtils.enableComboViewer(scanIdComboViewer, true); PluginUtils.updateFiltersEnabledAndCheckedState(toolBarActions.getFilterActions()); @@ -846,12 +871,12 @@ protected IStatus run(IProgressMonitor arg0) { } }; job.schedule(); - //After project selected and branches loaded reset the project list - if(resetStoredProjects) { + // After project selected and branches loaded reset the project list + if (resetStoredProjects) { storeCurrentProjects.add(selectedProject); preservCaretposition(storeCurrentProjects, selectedProject.getName()); currentProjects = storeCurrentProjects; - resetStoredProjects=false; + resetStoredProjects = false; } } } @@ -859,25 +884,28 @@ protected IStatus run(IProgressMonitor arg0) { // Add ModifyListener to handle manual text input for projects projectComboViewer.getCombo().addModifyListener(e -> { - if (isUpdatingCombo) return; + if (isUpdatingCombo) + return; String enteredProject = projectComboViewer.getCombo().getText().trim(); - + // Skip search if the text is the default instruction if (enteredProject.equals(PROJECT_COMBO_VIEWER_TEXT) || enteredProject.equals(LOADING_PROJECTS)) { updateStartScanButton(false); // Disable scan button return; } - // If user starts typing again and list is empty, restore currentProjects - if (projectComboViewer.getCombo().getItemCount() == 0 && !currentProjects.isEmpty() && enteredProject.length()>0) { - isUpdatingCombo = true; - preservCaretposition(currentProjects,enteredProject); - isUpdatingCombo = false; - } - + // If user starts typing again and list is empty, restore currentProjects + if (projectComboViewer.getCombo().getItemCount() == 0 && !currentProjects.isEmpty() + && enteredProject.length() > 0) { + isUpdatingCombo = true; + preservCaretposition(currentProjects, enteredProject); + isUpdatingCombo = false; + } + latestProjectSearchTerm = enteredProject; // Track the latest term List matchedProjects; matchedProjects = currentProjects.stream().map(Project::getName) - .filter(name -> name != null && name.toLowerCase().contains(enteredProject.toLowerCase())).limit(100) + .filter(name -> name != null && name.toLowerCase().contains(enteredProject.toLowerCase())) + .limit(100) .collect(Collectors.toList()); if (matchedProjects.isEmpty()) { CxLogger.info("Entered project is not exist in current projects list"); @@ -902,11 +930,11 @@ protected IStatus run(IProgressMonitor monitor) { isUpdatingCombo = true; // Update UI in UI thread if (searchedProjects != null && !searchedProjects.isEmpty()) { - preservCaretposition(searchedProjects,searchTerm); + preservCaretposition(searchedProjects, searchTerm); currentProjects = searchedProjects; - resetStoredProjects=true; + resetStoredProjects = true; } else { - preservCaretposition(Collections.emptyList(),searchTerm); + preservCaretposition(Collections.emptyList(), searchTerm); updateStartScanButton(false); // Disable scan button isUpdatingCombo = false; return; @@ -916,7 +944,7 @@ protected IStatus run(IProgressMonitor monitor) { }); } catch (Exception ex) { ex.printStackTrace(); - } + } return Status.OK_STATUS; } }; @@ -924,17 +952,19 @@ protected IStatus run(IProgressMonitor monitor) { } }; debounceTimer.schedule(pendingSearchTask, DEBOUNCE_DELAY_MS); - + } }); - // Add FocusListener to disable branch combo when project is cleared and focus lost + // Add FocusListener to disable branch combo when project is cleared and focus + // lost projectComboViewer.getCombo().addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { // When user clicks outside project combo, check if project is empty String enteredProject = projectComboViewer.getCombo().getText().trim(); - // If project field is empty or contains only the placeholder text, disable branch combo + // If project field is empty or contains only the placeholder text, disable + // branch combo if (enteredProject.isEmpty() || enteredProject.equals(PROJECT_COMBO_VIEWER_TEXT)) { currentProjectId = PluginConstants.EMPTY_STRING; PluginUtils.enableComboViewer(branchComboViewer, false); @@ -948,6 +978,7 @@ public void focusGained(FocusEvent e) { }); } + /** * Update state variables and make plugin fields loading when project changes * @@ -1070,7 +1101,7 @@ public void run() { } private void loadLatestScanByDefault(List scanList) { - if(scanList.isEmpty()) { + if (scanList.isEmpty()) { PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_NO_SCANS_AVAILABLE); return; } else { @@ -1081,12 +1112,13 @@ private void loadLatestScanByDefault(List scanList) { currentScanIdFormmated = getScanNameFromId(scanList, currentScanId); scanIdComboViewer.setSelection(new StructuredSelection(currentScanId)); PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanIdFormmated); - PluginUtils.showMessage(rootModel, resultsTree, String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, latestScanId)); - alreadyRunning=true; - updateResultsTree(currentScanId,false); + PluginUtils.showMessage(rootModel, resultsTree, + String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, latestScanId)); + alreadyRunning = true; + updateResultsTree(currentScanId, false); GlobalSettings.storeInPreferences(GlobalSettings.PARAM_SCAN_ID, currentScanId); }); - + } /** @@ -1137,7 +1169,7 @@ public void handleEvent(Event event) { public String getText(Object element) { if (element instanceof Scan) { // Always fetch the latest scan id from preferences before rendering - if(!GlobalSettings.getFromPreferences("LATEST_SCAN_ID", "").isEmpty()) { + if (!GlobalSettings.getFromPreferences("LATEST_SCAN_ID", "").isEmpty()) { latestScanId = GlobalSettings.getFromPreferences("LATEST_SCAN_ID", ""); } Scan scan = (Scan) element; @@ -1165,7 +1197,8 @@ protected IStatus run(IProgressMonitor arg0) { } if (selection.size() > 0) { sync.asyncExec(() -> { - PluginUtils.showMessage(rootModel, resultsTree, String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, selectedScan.getId())); + PluginUtils.showMessage(rootModel, resultsTree, String + .format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, selectedScan.getId())); PluginUtils.enableComboViewer(projectComboViewer, false); PluginUtils.enableComboViewer(branchComboViewer, false); }); @@ -1221,22 +1254,22 @@ private String getScanNameFromId(List scans, String scanId) { private String formatScanLabel(Scan scan) { String formattedString = ""; String updatedAtDate = PluginUtils.convertStringTimeStamp(scan.getUpdatedAt()); - if(!latestScanId.isEmpty() && scan.getId().equalsIgnoreCase(latestScanId)) { - formattedString = String.format(FORMATTED_SCAN_LABEL_LATEST, updatedAtDate ,scan.getId() ,"latest"); + if (!latestScanId.isEmpty() && scan.getId().equalsIgnoreCase(latestScanId)) { + formattedString = String.format(FORMATTED_SCAN_LABEL_LATEST, updatedAtDate, scan.getId(), "latest"); } else { - - formattedString = String.format(FORMATTED_SCAN_LABEL, updatedAtDate, scan.getId()); + + formattedString = String.format(FORMATTED_SCAN_LABEL, updatedAtDate, scan.getId()); } return formattedString; - + } - + /** * Retrieve latest scan from scanList */ - + private Scan getLatestScanFromScanList(List scanList) { - + return scanList.get(0); } @@ -1245,30 +1278,30 @@ private Scan getLatestScanFromScanList(List scanList) { * on the chosen scan id */ private void setSelectionForProjectComboViewer() { - - if(scanIdComboViewer.getCombo().getText().isEmpty()) { + + if (scanIdComboViewer.getCombo().getText().isEmpty()) { PluginUtils.clearMessage(rootModel, resultsTree); PluginUtils.showMessage(rootModel, resultsTree, PluginConstants.NO_SCAN_ID_PROVIDED); CxLogger.info(String.format(PluginConstants.NO_SCAN_ID_PROVIDED, PluginConstants.EMPTY_STRING)); return; } - + String scanIdText = scanIdComboViewer.getCombo().getText().trim(); String[] parts = scanIdText.split("\\s+"); if (parts.length >= 3) { - scanIdText = parts[2]; + scanIdText = parts[2]; } - + final String scanId = scanIdText; if (currentScanId.equals(scanId)) { - PluginUtils.clearMessage(rootModel, resultsTree); - // reload cached results - List results = DataProvider.getInstance().sortResults(); + PluginUtils.clearMessage(rootModel, resultsTree); + // reload cached results + List results = DataProvider.getInstance().sortResults(); - rootModel.setChildren(results); - resultsTree.refresh(); + rootModel.setChildren(results); + resultsTree.refresh(); PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanIdFormmated); CxLogger.info(String.format(PluginConstants.INFO_RESULTS_ALREADY_RETRIEVED, scanId)); return; @@ -1305,15 +1338,18 @@ protected IStatus run(IProgressMonitor arg0) { if (projectList.isEmpty()) return null; - // Fetch the project directly by ID — the full list may not contain it (e.g. pagination limits) + // Fetch the project directly by ID — the full list may not contain it (e.g. + // pagination limits) Project fetchedProject = DataProvider.getInstance().getProjectById(projectId); - // Determine project name: prefer the directly-fetched result, fall back to list lookup + // Determine project name: prefer the directly-fetched result, fall back to list + // lookup String projectName = (fetchedProject != null) ? fetchedProject.getName() : getProjectFromId(projectList, projectId); - // If the project was not already in the list, prepend it so it's visible in the dropdown + // If the project was not already in the list, prepend it so it's visible in the + // dropdown if (fetchedProject != null && projectList.stream().noneMatch(p -> p.getId().equals(projectId))) { projectList = new ArrayList<>(projectList); projectList.add(0, fetchedProject); @@ -1442,7 +1478,8 @@ public void selectionChanged(SelectionChangedEvent event) { @Override protected IStatus run(IProgressMonitor arg0) { - if (selectedItem.getResult() != null && selectedItem.getResult().getSimilarityId() != null) { + if (selectedItem.getResult() != null + && selectedItem.getResult().getSimilarityId() != null) { sync.asyncExec(() -> { currentlyDisplayedItem = selectedItem; createTriageSeverityAndStateCombos(selectedItem); @@ -1479,10 +1516,9 @@ private void createTriageSeverityAndStateCombos(DisplayModel selectedItem) { selectedSeverity = selectedItem.getSeverity(); String[] severity = { "CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO" }; - triageSeverityComboViewew.setContentProvider(ArrayContentProvider.getInstance()); - triageSeverityComboViewew.setInput(severity); - PluginUtils.setTextForComboViewer(triageSeverityComboViewew, currentSeverity); - + triageSeverityComboViewew.setContentProvider(ArrayContentProvider.getInstance()); + triageSeverityComboViewew.setInput(severity); + PluginUtils.setTextForComboViewer(triageSeverityComboViewew, currentSeverity); if (triageSeverityComboViewerListener != null) { triageSeverityComboViewew.removeSelectionChangedListener(triageSeverityComboViewerListener); @@ -1501,14 +1537,13 @@ public void selectionChanged(SelectionChangedEvent event) { String currentState = selectedItem.getState(); selectedState = selectedItem.getResult().getState(); - + // [AST-92100] Fetch dynamic states from DataProvider List state = DataProvider.getInstance().getStatesForEngine(selectedItem.getType()); - - triageStateComboViewer.setContentProvider(ArrayContentProvider.getInstance()); - triageStateComboViewer.setInput(state); - PluginUtils.setTextForComboViewer(triageStateComboViewer, currentState); - + + triageStateComboViewer.setContentProvider(ArrayContentProvider.getInstance()); + triageStateComboViewer.setInput(state); + PluginUtils.setTextForComboViewer(triageStateComboViewer, currentState); if (triageStateComboViewerListener != null) { triageStateComboViewer.removeSelectionChangedListener(triageStateComboViewerListener); @@ -1525,7 +1560,7 @@ public void selectionChanged(SelectionChangedEvent event) { triageStateComboViewer.addSelectionChangedListener(triageStateComboViewerListener); if (triageButtonAdapter != null) { triageButton.removeSelectionListener(triageButtonAdapter); - + } triageButtonAdapter = new SelectionAdapter() { @Override @@ -1543,14 +1578,17 @@ public void widgetSelected(SelectionEvent event) { Job job = new Job("Checkmarx: Updating triage information...") { String comment = commentText.getText() != null - && !commentText.getText().equalsIgnoreCase("Notes (Optional or required based on tenant configuration)") ? commentText.getText() - : ""; + && !commentText.getText() + .equalsIgnoreCase("Notes (Optional or required based on tenant configuration)") + ? commentText.getText() + : ""; @Override protected IStatus run(IProgressMonitor arg0) { try { - DataProvider.getInstance().triageUpdate(projectId,similarityId, engineType, selectedState, comment, selectedSeverity); - + DataProvider.getInstance().triageUpdate(projectId, similarityId, engineType, + selectedState, comment, selectedSeverity); + sync.asyncExec(() -> { selectedItem.setSeverity(selectedSeverity); selectedItem.setState(selectedState); @@ -1568,7 +1606,9 @@ protected IStatus run(IProgressMonitor arg0) { }); } catch (Exception e) { sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), "Triage failed", e.getMessage(), null, null, null).open(); + new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow() + .getShell().getDisplay(), "Triage failed", e.getMessage(), null, null, null) + .open(); }); } @@ -1594,10 +1634,9 @@ protected IStatus run(IProgressMonitor arg0) { } } }; - + triageButton.addSelectionListener(triageButtonAdapter); - - + boolean isSCAVulnerability = selectedItem.getType().equalsIgnoreCase(PluginConstants.SCA_DEPENDENCY); triageButton.setVisible(!isSCAVulnerability); triageButton.setEnabled(!isSCAVulnerability); @@ -1658,26 +1697,32 @@ protected IStatus run(IProgressMonitor arg0) { openLink(codeBashing.getPath()); } catch (CxException e) { CxLogger.info(String.format(PluginConstants.CODEBASHING, e.getMessage())); - + if (e.getExitCode() == PluginConstants.EXIT_CODE_LICENSE_NOT_FOUND) { SelectionAdapter onClickCodebashingLink = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { try { - PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(event.text)); + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser() + .openURL(new URL(event.text)); } catch (PartInitException | MalformedURLException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_CODEBASHING_DETAILS, e.getMessage()), e); + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_CODEBASHING_DETAILS, + e.getMessage()), e); } } }; - + sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), PluginConstants.CODEBASHING, + new NotificationPopUpUI( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), + PluginConstants.CODEBASHING, PluginConstants.CODEBASHING_NO_LICENSE, onClickCodebashingLink, null, null).open(); }); } else if (e.getExitCode() == PluginConstants.EXIT_CODE_LESSON_NOT_FOUND) { sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), PluginConstants.CODEBASHING, + new NotificationPopUpUI( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), + PluginConstants.CODEBASHING, PluginConstants.CODEBASHING_NO_LESSON, null, null, null).open(); }); } @@ -1731,7 +1776,9 @@ public void run() { Text descriptionTxt = new Text(detailsComposite, SWT.READ_ONLY | SWT.WRAP | SWT.MULTI); descriptionTxt.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); - descriptionTxt.setText(selectedItem.getResult().getDescription() != null ? selectedItem.getResult().getDescription(): "No data"); + descriptionTxt.setText( + selectedItem.getResult().getDescription() != null ? selectedItem.getResult().getDescription() + : "No data"); descriptionScrolledComposite.setContent(detailsComposite); descriptionScrolledComposite.setMinSize(descriptionScrolledComposite.getSize().x, @@ -1754,7 +1801,8 @@ public void run() { tbtmChanges.setControl(changesScrolledComposite); changesScrolledComposite.setContent(changesComposite); - changesScrolledComposite.setMinSize(changesScrolledComposite.getSize().x, changesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y); + changesScrolledComposite.setMinSize(changesScrolledComposite.getSize().x, + changesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y); scrolledComposite.setContent(tabFolder); @@ -1775,7 +1823,8 @@ protected void populateLoadingScreen() { sync.asyncExec(() -> { Composite loadingScreen = new Composite(scrolledComposite, SWT.NONE); loadingScreen.setLayout(new GridLayout(1, false)); - loadingScreen.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, false, false)); + loadingScreen + .setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, false, false)); CLabel loadingLabel = new CLabel(loadingScreen, SWT.NONE); loadingLabel.setText(PluginConstants.LOADING_CHANGES); @@ -1961,7 +2010,7 @@ private void updateAttackVectorForSelectedTreeItem(DisplayModel selectedItem) { if (selectedItem.getType().equalsIgnoreCase(PluginConstants.SAST)) { drawAttackVector(selectedItem); } - + layoutAttackVectorItemComposite(); }); } @@ -1976,18 +2025,18 @@ private void drawPackageData(DisplayModel selectedItem) { Composite child = new Composite(sc, SWT.NONE); child.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - child.setLayout(new GridLayout(1, false)); + child.setLayout(new GridLayout(1, false)); child.setBackground(attackVectorCompositePanel.getBackground()); drawAttackVectorTitle(child, PluginConstants.PACKAGE_DATA); drawIndividualPackageData(child, selectedItem.getResult().getData().getPackageData()); - + sc.setContent(child); sc.setMinSize(child.computeSize(SWT.DEFAULT, SWT.DEFAULT)); sc.setExpandHorizontal(true); sc.setExpandVertical(true); } - + /** * Draw attack vector title * @@ -2000,9 +2049,9 @@ private void drawAttackVectorTitle(Composite parent, String title) { attackVectorLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1)); attackVectorLabel.setBackground(attackVectorCompositePanel.getBackground()); attackVectorLabel.setText(title); - + attackVectorLabel.layout(); - + drawAttackVectorSeparator(parent); } @@ -2034,30 +2083,32 @@ private void drawAttackVector(DisplayModel selectedItem) { final TabItem remediationExamplesTab = new TabItem(tabFolder, SWT.NONE); remediationExamplesTab.setText(PluginConstants.REMEDIATION_EXAMPLES); - + drawSASTAttackVector(selectedItem, tabFolder, attackVectorTab); - + learnMoreData = null; - + tabFolder.addSelectionListener(new SelectionListener() { @Override - public void widgetDefaultSelected(SelectionEvent arg0) {} + public void widgetDefaultSelected(SelectionEvent arg0) { + } @Override public void widgetSelected(SelectionEvent event) { String tab = event.item != null ? ((TabItem) event.item).getText() : StringUtils.EMPTY; - + switch (tab) { - case PluginConstants.LEARN_MORE: + case PluginConstants.LEARN_MORE: drawSASTLearnMore(selectedItem, tabFolder, learnMoreTab); case PluginConstants.REMEDIATION_EXAMPLES: drawSASTRemediationExamples(selectedItem, tabFolder, remediationExamplesTab); - default: return; + default: + return; } } }); } - + /** * Draw SAST Attack Vector tab * @@ -2066,25 +2117,26 @@ public void widgetSelected(SelectionEvent event) { * @param attackVectorTab */ private void drawSASTAttackVector(DisplayModel selectedItem, TabFolder folder, TabItem attackVectorTab) { - final ScrolledComposite attackVectorScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL | SWT.H_SCROLL); + final ScrolledComposite attackVectorScrolledComposite = new ScrolledComposite(folder, + SWT.V_SCROLL | SWT.H_SCROLL); attackVectorScrolledComposite.setExpandVertical(true); attackVectorScrolledComposite.setExpandHorizontal(true); attackVectorTab.setControl(attackVectorScrolledComposite); final Composite attackVectorComposite = new Composite(attackVectorScrolledComposite, SWT.NONE); attackVectorComposite.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - attackVectorComposite.setLayout(new GridLayout(1, false)); + attackVectorComposite.setLayout(new GridLayout(1, false)); attackVectorScrolledComposite.setContent(attackVectorComposite); - + String queryName = selectedItem.getResult().getData().getQueryName(); String groupName = selectedItem.getResult().getData().getGroup(); List nodesList = selectedItem.getResult().getData().getNodes(); drawIndividualAttackVectorData(attackVectorComposite, queryName, groupName, nodesList, false); - + attackVectorScrolledComposite.setMinSize(attackVectorComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); } - + /** * Draw SAST Learn More tab * @@ -2092,39 +2144,44 @@ private void drawSASTAttackVector(DisplayModel selectedItem, TabFolder folder, T * @param folder * @param learnMoreTab */ - private void drawSASTLearnMore(DisplayModel selectedItem, TabFolder folder, TabItem learnMoreTab) { + private void drawSASTLearnMore(DisplayModel selectedItem, TabFolder folder, TabItem learnMoreTab) { final ScrolledComposite learnMoreScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL); learnMoreScrolledComposite.setExpandHorizontal(true); learnMoreScrolledComposite.setExpandVertical(true); - + final Composite learnMoreComposite = new Composite(learnMoreScrolledComposite, SWT.NONE); learnMoreComposite.setLayout(new GridLayout()); - + learnMoreScrolledComposite.setContent(learnMoreComposite); learnMoreScrolledComposite.setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - - if(learnMoreData == null) { + + if (learnMoreData == null) { CLabel loadingLabel = new CLabel(learnMoreComposite, SWT.NONE); loadingLabel.setText(PluginConstants.LEARN_MORE_LOADING); } - + learnMoreTab.setControl(learnMoreScrolledComposite); - + Job job = new Job(PluginConstants.GETTING_LEARN_MORE_JOB) { @Override protected IStatus run(IProgressMonitor arg0) { sync.asyncExec(() -> { try { - - List learnMoreData = getLearnMoreData(selectedItem.getResult().getData().getQueryId()); - + + List learnMoreData = getLearnMoreData( + selectedItem.getResult().getData().getQueryId()); + clearLearnMoreComposite(learnMoreComposite); - - for(LearnMore learnMore : learnMoreData) { - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_RISK, learnMore.getRisk().trim()); - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_CAUSE, learnMore.getCause().trim()); - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_GENERAL_RECOMMENDATIONS, learnMore.getGeneralRecommendations().trim()); - + + for (LearnMore learnMore : learnMoreData) { + addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_RISK, + learnMore.getRisk().trim()); + addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_CAUSE, + learnMore.getCause().trim()); + addLearnMoreSectionsToComposite(learnMoreComposite, + PluginConstants.LEARN_MORE_GENERAL_RECOMMENDATIONS, + learnMore.getGeneralRecommendations().trim()); + // Adding CWE link in Learn More section of SAST vulnerability String cweId = selectedItem.getResult().getVulnerabilityDetails().getCweId(); if (cweId != null && !cweId.isEmpty()) { @@ -2141,27 +2198,28 @@ protected IStatus run(IProgressMonitor arg0) { }); } - learnMoreScrolledComposite.setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - learnMoreComposite.layout(); + learnMoreScrolledComposite + .setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + learnMoreComposite.layout(); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_LEARN_MORE, e.getMessage()), e); - + clearLearnMoreComposite(learnMoreComposite); - + Label learnMoreErrorLabel = new Label(learnMoreComposite, SWT.NONE); learnMoreErrorLabel.setText(e.getMessage()); learnMoreScrolledComposite.setContent(learnMoreComposite); } }); - + return Status.OK_STATUS; } }; job.schedule(); } - + /** * Get and cache learn more data * @@ -2169,16 +2227,16 @@ protected IStatus run(IProgressMonitor arg0) { * @return * @throws Exception */ - private static List getLearnMoreData(String queryId) throws Exception{ - if(learnMoreData != null) { + private static List getLearnMoreData(String queryId) throws Exception { + if (learnMoreData != null) { return learnMoreData; } - + learnMoreData = DataProvider.getInstance().learnMore(queryId); - + return learnMoreData; } - + /** * Draw SAST Remediation Examples tab * @@ -2186,95 +2244,104 @@ private static List getLearnMoreData(String queryId) throws Exception * @param folder * @param remediationExamplesTab */ - private void drawSASTRemediationExamples(DisplayModel selectedItem, TabFolder folder, TabItem remediationExamplesTab) { - final ScrolledComposite remediationExamplesScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL | SWT.BORDER); + private void drawSASTRemediationExamples(DisplayModel selectedItem, TabFolder folder, + TabItem remediationExamplesTab) { + final ScrolledComposite remediationExamplesScrolledComposite = new ScrolledComposite(folder, + SWT.V_SCROLL | SWT.BORDER); remediationExamplesScrolledComposite.setExpandHorizontal(true); remediationExamplesScrolledComposite.setExpandVertical(true); - + final Composite remediationExamplesComposite = new Composite(remediationExamplesScrolledComposite, SWT.NONE); remediationExamplesComposite.setLayout(new GridLayout()); - + remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); - remediationExamplesScrolledComposite.setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - - if(learnMoreData == null) { + remediationExamplesScrolledComposite + .setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + + if (learnMoreData == null) { Label loadingLabel = new Label(remediationExamplesComposite, SWT.NONE); loadingLabel.setText(PluginConstants.LEARN_MORE_LOADING); } - + remediationExamplesTab.setControl(remediationExamplesScrolledComposite); - + Job job = new Job(PluginConstants.GETTING_LEARN_MORE_JOB) { @Override protected IStatus run(IProgressMonitor arg0) { sync.asyncExec(() -> { try { - - List learnMoreData = getLearnMoreData(selectedItem.getResult().getData().getQueryId()); - + + List learnMoreData = getLearnMoreData( + selectedItem.getResult().getData().getQueryId()); + clearLearnMoreComposite(remediationExamplesComposite); - - for(LearnMore learnMore : learnMoreData) { + + for (LearnMore learnMore : learnMoreData) { List samples = learnMore.getSamples(); - - if(samples.size() == 0 ) { + + if (samples.size() == 0) { Label noRemediationLabel = new Label(remediationExamplesComposite, SWT.NONE); noRemediationLabel.setText(PluginConstants.NO_REMEDIATION_EXAMPLES); remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); - + continue; } - - for(Sample sample : samples) { + + for (Sample sample : samples) { StyledText sampleTitle = new StyledText(remediationExamplesComposite, SWT.WRAP); - sampleTitle.setText(String.format(PluginConstants.REMEDIATION_EXAMPLE_TITLE_FORMAT, sample.getTitle(), sample.getProgLanguage())); - GridData titleLayoutData = new GridData( GridData.FILL_HORIZONTAL ) ; + sampleTitle.setText(String.format(PluginConstants.REMEDIATION_EXAMPLE_TITLE_FORMAT, + sample.getTitle(), sample.getProgLanguage())); + GridData titleLayoutData = new GridData(GridData.FILL_HORIZONTAL); titleLayoutData.grabExcessHorizontalSpace = true; titleLayoutData.horizontalAlignment = SWT.FILL; - titleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width - SCROLL_WIDTH; + titleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width + - SCROLL_WIDTH; titleLayoutData.horizontalSpan = 2; sampleTitle.setLayoutData(titleLayoutData); sampleTitle.setMargins(2, 5, 2, 5); - - Composite sampleExampleComposite = new Composite(remediationExamplesComposite, SWT.NONE); + + Composite sampleExampleComposite = new Composite(remediationExamplesComposite, + SWT.NONE); sampleExampleComposite.setBackground(remediationExamplesComposite.getBackground()); GridLayout layout = new GridLayout(); layout.marginHeight = 10; layout.marginWidth = 10; sampleExampleComposite.setLayout(layout); sampleExampleComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - + Label sampleExample = new Label(sampleExampleComposite, SWT.WRAP); - sampleExample.setText(sample.getCode()); - GridData exampleLayoutData = new GridData(GridData.FILL_HORIZONTAL) ; + sampleExample.setText(sample.getCode()); + GridData exampleLayoutData = new GridData(GridData.FILL_HORIZONTAL); exampleLayoutData.grabExcessHorizontalSpace = true; exampleLayoutData.horizontalAlignment = SWT.FILL; - exampleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width - SCROLL_WIDTH; + exampleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width + - SCROLL_WIDTH; exampleLayoutData.horizontalSpan = 2; sampleExample.setLayoutData(exampleLayoutData); - - remediationExamplesScrolledComposite.setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + + remediationExamplesScrolledComposite + .setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); remediationExamplesComposite.layout(); } } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_LEARN_MORE, e.getMessage()), e); - + clearLearnMoreComposite(remediationExamplesComposite); - + Label remediationErrorLabel = new Label(remediationExamplesComposite, SWT.NONE); remediationErrorLabel.setText(e.getMessage()); remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); } }); - + return Status.OK_STATUS; } }; job.schedule(); } - + /** * Clear Learn More composite * @@ -2285,7 +2352,7 @@ private void clearLearnMoreComposite(Composite learnMoreComposite) { child.dispose(); } } - + /** * Add Learn More sections to composite (Risk, Cause, General Recommendations) * @@ -2296,11 +2363,11 @@ private void clearLearnMoreComposite(Composite learnMoreComposite) { */ private void addLearnMoreSectionsToComposite(Composite composite, String title, String description) { Label titleLabel = new Label(composite, SWT.WRAP); - titleLabel.setText(title); + titleLabel.setText(title); titleLabel.setFont(boldFont); StyledText descriptionLabel = new StyledText(composite, SWT.WRAP); - descriptionLabel.setText(description); + descriptionLabel.setText(description); GridData descriptionLayout = new GridData(GridData.FILL_HORIZONTAL); descriptionLayout.grabExcessHorizontalSpace = true; descriptionLayout.horizontalAlignment = SWT.FILL; @@ -2310,15 +2377,18 @@ private void addLearnMoreSectionsToComposite(Composite composite, String title, descriptionLabel.setBottomMargin(20); } - /*private void populateBFLMessage(Image image, String bflMessage) { - bflLabel.setImage(image); - bflText.setText(bflMessage); - bflLabel.layout(); - bflText.requestLayout(); - - }*/ + /* + * private void populateBFLMessage(Image image, String bflMessage) { + * bflLabel.setImage(image); + * bflText.setText(bflMessage); + * bflLabel.layout(); + * bflText.requestLayout(); + * + * } + */ - private void drawIndividualAttackVectorData(Composite parent, String queryName, String groupName, List nodesList, Boolean populateBFLNode) { + private void drawIndividualAttackVectorData(Composite parent, String queryName, String groupName, + List nodesList, Boolean populateBFLNode) { if (nodesList != null && !nodesList.isEmpty()) { for (int i = 0; i < nodesList.size(); i++) { @@ -2326,7 +2396,8 @@ private void drawIndividualAttackVectorData(Composite parent, String queryName, Composite listComposite = createRowComposite(parent); - CLabel label = createRowLabel(listComposite, String.format("%s | %s", i + 1, node.getName()), populateBFLNode ? i == bflNode : false); + CLabel label = createRowLabel(listComposite, String.format("%s | %s", i + 1, node.getName()), + populateBFLNode ? i == bflNode : false); Link attackVectorValueLinkText = createRowLink(listComposite, String.format("%s[%d,%d]", node.getFileName(), node.getLine(), node.getColumn()), @@ -2344,56 +2415,61 @@ public void handleEvent(Event event) { } } - /*private void populateBFLNode(Composite parent, DisplayModel selectedItem) { - - Job job = new Job("Loading BFL node") { - - Composite itemComposite; - - @Override - protected IStatus run(IProgressMonitor arg0) { - - try { - bflNode = DataProvider.getInstance().getBestFixLocation(UUID.fromString(currentScanId), - selectedItem.getResult().getData().getQueryId(), - selectedItem.getResult().getData().getNodes()); - String queryName = selectedItem.getResult().getData().getQueryName(); - String groupName = selectedItem.getResult().getData().getGroup(); - List nodesList = selectedItem.getResult().getData().getNodes(); - - sync.asyncExec(() -> { - if (bflNode != -1) { - parent.dispose(); - itemComposite = createAttackVectorComposite(); - populateBFLMessage(BFL, PluginConstants.BFL_FOUND); - drawIndividualAttackVectorData(itemComposite, queryName, groupName, nodesList, true); - } else { - populateBFLMessage(null, PluginConstants.BFL_NOT_FOUND); - } - - }); - } catch (Exception e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BEST_FIX_LOCATION, e.getMessage()), e); - } - return Status.OK_STATUS; - } - - }; - - job.schedule(); - }*/ + /* + * private void populateBFLNode(Composite parent, DisplayModel selectedItem) { + * + * Job job = new Job("Loading BFL node") { + * + * Composite itemComposite; + * + * @Override + * protected IStatus run(IProgressMonitor arg0) { + * + * try { + * bflNode = + * DataProvider.getInstance().getBestFixLocation(UUID.fromString(currentScanId), + * selectedItem.getResult().getData().getQueryId(), + * selectedItem.getResult().getData().getNodes()); + * String queryName = selectedItem.getResult().getData().getQueryName(); + * String groupName = selectedItem.getResult().getData().getGroup(); + * List nodesList = selectedItem.getResult().getData().getNodes(); + * + * sync.asyncExec(() -> { + * if (bflNode != -1) { + * parent.dispose(); + * itemComposite = createAttackVectorComposite(); + * populateBFLMessage(BFL, PluginConstants.BFL_FOUND); + * drawIndividualAttackVectorData(itemComposite, queryName, groupName, + * nodesList, true); + * } else { + * populateBFLMessage(null, PluginConstants.BFL_NOT_FOUND); + * } + * + * }); + * } catch (Exception e) { + * CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BEST_FIX_LOCATION, + * e.getMessage()), e); + * } + * return Status.OK_STATUS; + * } + * + * }; + * + * job.schedule(); + * } + */ - private void drawVulnerabilityLocation(DisplayModel selectedItem) { + private void drawVulnerabilityLocation(DisplayModel selectedItem) { ScrolledComposite sc = new ScrolledComposite(attackVectorCompositePanel, SWT.H_SCROLL | SWT.V_SCROLL); Composite child = new Composite(sc, SWT.NONE); child.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - child.setLayout(new GridLayout(1, false)); + child.setLayout(new GridLayout(1, false)); child.setBackground(attackVectorCompositePanel.getBackground()); drawAttackVectorTitle(child, PluginConstants.LOCATION); drawIndividualLocationData(child, selectedItem); - + sc.setContent(child); sc.setMinSize(child.computeSize(SWT.DEFAULT, SWT.DEFAULT)); sc.setExpandHorizontal(true); @@ -2448,7 +2524,7 @@ private CLabel createRowLabel(Composite rowComposite, String text, Boolean isBfl label.setFont(boldFont); label.setText(text); label.requestLayout(); - + return label; } @@ -2701,7 +2777,7 @@ protected IStatus run(IProgressMonitor arg0) { // Clear vulnerabilities from Problems View PluginUtils.clearVulnerabilitiesFromProblemsView(); - + toolBarActions.refreshToolbar(); } @@ -2780,7 +2856,7 @@ public void run() { } } }); - + return Status.OK_STATUS; } @@ -2800,7 +2876,8 @@ private void enablePluginFields(boolean enableBranchCombobox) { for (Action action : toolBarActions.getToolBarActions()) { String actionName = action.getId(); - if (actionName.equals(ActionName.GROUP_BY_SEVERITY.name()) && !actionName.equals(ActionName.GROUP_BY_QUERY_NAME.name())) { + if (actionName.equals(ActionName.GROUP_BY_SEVERITY.name()) + && !actionName.equals(ActionName.GROUP_BY_QUERY_NAME.name())) { continue; } @@ -2813,33 +2890,62 @@ 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); + boolean isAuthenticated = Preferences.isAuthenticated(); + + // Handle case: credentials just set (plugin panel not yet drawn) + if (isAuthenticated && !isPluginDraw) { + CxLogger.info("Credentials detected, drawing plugin panel"); + drawPluginPanel(); + lastApiKey = currentApiKey; + return; + } + + // Handle case: credentials just removed (plugin panel is drawn) + if (!isAuthenticated && 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: not authenticated and panel not drawn (initial state) + if (!isAuthenticated && !isPluginDraw) { + // Already showing missing credentials panel, nothing to do + lastApiKey = currentApiKey; + return; + } + + // Handle case: API key changed but still authenticated (plugin already drawn) + if (isAuthenticated && 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; - } + }); } /** @@ -2856,36 +2962,40 @@ private List getProjects() { } catch (Exception e) { String errorMessage = e.getCause() != null && e.getCause().getMessage() != null ? e.getCause().getMessage() : e.getMessage(); - PluginUtils.showMessage(rootModel, resultsTree, errorMessage); + org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { + PluginUtils.showMessage(rootModel, resultsTree, errorMessage); + }); } return projectList; } - + /** * Update scan button with proper tooltip * * @param enabled */ - private void updateStartScanButton(boolean enabled) { - if(enabled) { - String runningScanId = GlobalSettings.getFromPreferences(GlobalSettings.PARAM_RUNNING_SCAN_ID, PluginConstants.EMPTY_STRING); - boolean isScanRunning = StringUtils.isNoneEmpty(runningScanId); - boolean branchSelected = StringUtils.isNotBlank(GlobalSettings.getFromPreferences(GlobalSettings.PARAM_BRANCH, PluginConstants.EMPTY_STRING)); - + private void updateStartScanButton(boolean enabled) { + if (enabled) { + String runningScanId = GlobalSettings.getFromPreferences(GlobalSettings.PARAM_RUNNING_SCAN_ID, + PluginConstants.EMPTY_STRING); + boolean isScanRunning = StringUtils.isNoneEmpty(runningScanId); + boolean branchSelected = StringUtils.isNotBlank( + GlobalSettings.getFromPreferences(GlobalSettings.PARAM_BRANCH, PluginConstants.EMPTY_STRING)); + toolBarActions.getStartScanAction().setEnabled(!isScanRunning && branchSelected); } else { toolBarActions.getStartScanAction().setEnabled(false); } } - + /** * Store the user entered value and the caret position * * @param projectList * @param searchText */ - private void preservCaretposition( List projectList, String searchText) { + private void preservCaretposition(List projectList, String searchText) { int caretPos = projectComboViewer.getCombo().getCaretPosition(); projectComboViewer.setInput(projectList); PluginUtils.setTextForComboViewer(projectComboViewer, searchText); 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..534ef180 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 @@ -15,8 +15,6 @@ import java.util.stream.Collectors; import org.eclipse.jgit.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.checkmarx.ast.codebashing.CodeBashing; import com.checkmarx.ast.learnMore.LearnMore; @@ -27,13 +25,10 @@ import com.checkmarx.ast.results.result.Node; import com.checkmarx.ast.results.result.Result; import com.checkmarx.ast.scan.Scan; -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.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.filters.FilterState; @@ -60,6 +55,8 @@ public class DataProvider { private List currentResultsTransformed; private List platformStates = new ArrayList<>(); + private WrapperProvider wrapperProvider = new WrapperProvider(); + /** * Singleton data provider instance * @@ -97,30 +94,26 @@ public void setCurrentResults(Results currentResults) { */ public List getProjects() throws Exception { List projectList = new ArrayList(); - - CxWrapper cxWrapper = authenticateWithAST(); - - if (cxWrapper != null) { - try { - projectList = cxWrapper.projectList(LIMIT_FILTER); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); - } + try { + projectList = wrapperProvider.getProjects(LIMIT_FILTER); + + } catch (IOException | InterruptedException | CxException exception) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, exception.getMessage()), exception); + throw exception; } return projectList; } - + /** * Fetch a single project directly by its ID using the project show command. * Returns null if the project cannot be retrieved. */ public Project getProjectById(String projectId) { try { - CxWrapper cxWrapper = getWrapper(); - if (cxWrapper != null && projectId != null && !projectId.isEmpty()) { - return cxWrapper.projectShow(UUID.fromString(projectId)); + if (projectId != null && !projectId.isEmpty()) { + return wrapperProvider.projectShow(UUID.fromString(projectId)); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); @@ -136,101 +129,72 @@ public Project getProjectById(String projectId) { */ public List getProjects(String projectName) throws Exception { List projectList = new ArrayList(); - - CxWrapper cxWrapper = authenticateWithAST(); + String filterProject = NAME_FILTER+projectName; - - if (cxWrapper != null) { - try { - projectList = cxWrapper.projectList(filterProject); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); - } + try { + projectList = wrapperProvider.getProjects(filterProject); + + } catch (IOException | InterruptedException | CxException exception) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, exception.getMessage()), exception); + throw exception; } return projectList; } - + /** * Get the codeBashing link - * @throws Exception + * @throws Exception */ - + public CodeBashing getCodeBashingLink(String cwe, String language, String queryName) throws CxException, Exception { - CxWrapper cxWrapper = getWrapper(); - - return cxWrapper.codeBashingList(cwe, language, queryName).get(0); + return wrapperProvider.codeBashingList(cwe, language, queryName).get(0); } - + /** * Get branches for a specific project - * + * * @param projectId * @return */ public List getBranchesForProject(String projectId) { this.projectId = projectId; List branchList = new ArrayList(); - + try { - CxWrapper cxWrapper = getWrapper(); - if(!StringUtils.isEmptyOrNull(projectId)) { - branchList = cxWrapper.projectBranches(UUID.fromString(projectId), PluginConstants.EMPTY_STRING); + branchList = wrapperProvider.projectBranches(UUID.fromString(projectId), PluginConstants.EMPTY_STRING); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BRANCHES, projectId, e.getMessage()), e); } - + return branchList; } - + /** * Get scans for a specific project based on a provided branch - * + * * @param branch * @return */ public List getScansForProject(String branch) { List scanList = new ArrayList<>(); - + try { String filter = String.format(FILTER_SCANS_FOR_PROJECT, projectId, branch); - CxWrapper cxWrapper = getWrapper(); - scanList = cxWrapper.scanList(filter); + scanList = wrapperProvider.scanList(filter); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_SCANS, projectId, branch, e.getMessage()), e); } - + return scanList; } - - /** - * Authenticate to One with current credentials - * @throws Exception - */ - private static CxWrapper authenticateWithAST() throws Exception { - CxWrapper cxWrapper = null; - - try { - - cxWrapper = getWrapper(); - String validationResult = cxWrapper.authValidate(); - CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, validationResult)); - - } catch (CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, e.getMessage()), e); - throw new Exception(e); - } - - return cxWrapper; - } - /** * Get results for a specific scan id * @@ -249,10 +213,9 @@ public List getResultsForScanId(String scanId) { CxLogger.warning("Failed to fetch all platform states on scan load: " + e.getMessage()); } - try { + try { CxLogger.info(String.format(PluginConstants.INFO_FETCHING_RESULTS, scanId)); - CxWrapper cxWrapper = getWrapper(); - scanResults = cxWrapper.results(UUID.fromString(scanId), ECLIPSE_AGENT); + scanResults = wrapperProvider.results(UUID.fromString(scanId), ECLIPSE_AGENT); setCurrentResults(scanResults); CxLogger.info(String.format(PluginConstants.INFO_SCAN_RESULTS_COUNT, scanResults.getTotalCount())); @@ -273,12 +236,10 @@ public List getResultsForScanId(String scanId) { */ public Scan getScanInformation(String scanId) throws Exception { Scan scan = null; - - CxWrapper cxWrapper = getWrapper(); - + try { CxLogger.info(String.format(PluginConstants.INFO_GETTING_SCAN_INFO, scanId)); - scan = cxWrapper.scanShow(UUID.fromString(scanId)); + scan = wrapperProvider.scanShow(UUID.fromString(scanId)); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_SCAN_INFO, e.getMessage()), e); throw new Exception(e); @@ -709,29 +670,6 @@ private int getParentCounter(List results) { } - /** - * Create a CxWrapper with current credentials - * - * @return - * @throws Exception - */ - private static CxWrapper getWrapper() throws Exception { - CxWrapper cxWrapper = null; - - Logger log = LoggerFactory.getLogger(Authenticator.class.getName()); - - CxConfig config = CxConfig.builder().apiKey(Preferences.getApiKey()).additionalParameters(Preferences.getAdditionalOptions()).build(); - - try { - cxWrapper = new CxWrapper(config, log); - } catch (IOException e) { - CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); - throw new Exception(e); - } - - return cxWrapper; - } - /** * Check if plugin has results loaded * @@ -746,37 +684,33 @@ public boolean containsResults() { */ public int getBestFixLocation(UUID scanId, String queryId, List bflNodes) throws Exception { - CxWrapper cxWrapper = authenticateWithAST(); - int bflNode = -1; - if(cxWrapper != null) { - bflNode = cxWrapper.getResultsBfl(scanId, queryId, bflNodes); + try{ + return wrapperProvider.getResultsBfl(scanId, queryId, bflNodes); + }catch(Exception ex){ + CxLogger.error(String.format("Exception occurred while getting resultsbfl. Root cause: %s", ex.getMessage()), ex); + return -1; } - return bflNode; } - + /** * Get One Triage details - * + * * @return - * @throws Exception + * @throws Exception */ public List getTriageShow(UUID projectID, String similarityID, String scanType) throws Exception { List triageList = new ArrayList(); - CxWrapper cxWrapper = authenticateWithAST(); - // TODO: remove this condition when CLI is updated to manage these checks if(scanType.equals(PluginConstants.KICS_INFRASTRUCTURE)) { scanType = "kics"; } - if (cxWrapper != null) { - try { - triageList = cxWrapper.triageShow(projectID, similarityID, scanType); + try { + triageList = wrapperProvider.triageShow(projectID, similarityID, scanType); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_TRIAGE_DETAILS, e.getMessage()), e); - } + } catch (IOException | InterruptedException | CxException e) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_TRIAGE_DETAILS, e.getMessage()), e); } return triageList; @@ -796,22 +730,18 @@ public List getTriageShow(UUID projectID, String similarityID, String public void triageUpdate(UUID projectId, String similarityId, String engineType, String state, String comment, String severity) throws Exception { try { - CxWrapper cxWrapper = authenticateWithAST(); - - if (cxWrapper != null) { - cxWrapper.triageUpdate(projectId, similarityId, engineType, state, comment, severity); - } + wrapperProvider.triageUpdate(projectId, similarityId, engineType, state, comment, severity); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_UPDATING_TRIAGE, e.getMessage()), e); throw new Exception(e.getMessage()); - + } } - - public List learnMore(String queryId) throws Exception { - return authenticateWithAST().learnMore(queryId); + + public List learnMore(String queryId) throws Exception { + return wrapperProvider.learnMore(queryId); } - + public Scan createScan(String sourcePath, String projectName, String branchName) throws IOException, InterruptedException, CxException, Exception { Map scanArguments = new HashMap<>(); scanArguments.put("-s", sourcePath); @@ -820,16 +750,16 @@ public Scan createScan(String sourcePath, String projectName, String branchName) scanArguments.put("--agent", ECLIPSE_AGENT); String additionalParameters = "--async --sast-incremental --resubmit"; - - return authenticateWithAST().scanCreate(scanArguments, additionalParameters); + + return wrapperProvider.scanCreate(scanArguments, additionalParameters); } - + public void cancelScan(String scanId) throws IOException, InterruptedException, CxException, Exception { - authenticateWithAST().scanCancel(scanId); + wrapperProvider.scanCancel(scanId); } - + public boolean isScanAllowed() throws CxException, IOException, InterruptedException, Exception { - return authenticateWithAST().ideScansEnabled(); + return wrapperProvider.ideScansEnabled(); } /** @@ -841,11 +771,10 @@ private List getAllStatesFromPlatform() throws Exception { return Collections.emptyList(); } - CxWrapper cxWrapper = authenticateWithAST(); List allStates = new ArrayList<>(); try { - List customStates = cxWrapper.triageGetStates(false); + List customStates = wrapperProvider.triageGetStates(false); allStates = customStates.stream().map(CustomState::getName).collect(Collectors.toList()); } catch (Exception e) { CxLogger.warning("Could not fetch platform states: " + e.getMessage()); 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..15511ba7 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 @@ -21,10 +21,11 @@ import org.eclipse.ui.PlatformUI; import com.checkmarx.eclipse.enums.ActionName; +import com.checkmarx.eclipse.enums.GroupingMode; 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; @@ -152,33 +153,33 @@ private void createGroupByActions() { groupBySeverityAction = new Action(GROUP_BY_SEVERITY, IAction.AS_CHECK_BOX) { @Override public void run() { - FilterState.setState(Severity.GROUP_BY_SEVERITY); + FilterState.setGroupingMode(GroupingMode.SEVERITY); pluginEventBus.post(new PluginListenerDefinition(PluginListenerType.FILTER_CHANGED, DataProvider.getInstance().sortResults())); } }; - + groupBySeverityAction.setId(ActionName.GROUP_BY_SEVERITY.name()); groupBySeverityAction.setChecked(FilterState.groupBySeverity); groupByQueryNameAction = new Action(GROUP_BY_QUERY_NAME, IAction.AS_CHECK_BOX) { @Override public void run() { - FilterState.setState(Severity.GROUP_BY_QUERY_NAME); + FilterState.setGroupingMode(GroupingMode.QUERY_NAME); pluginEventBus.post(new PluginListenerDefinition(PluginListenerType.FILTER_CHANGED, DataProvider.getInstance().sortResults())); } }; - + groupByQueryNameAction.setId(ActionName.GROUP_BY_QUERY_NAME.name()); groupByQueryNameAction.setChecked(FilterState.groupByQueryName); - + groupByStateNameAction = new Action(GROUP_BY_STATE_NAME, IAction.AS_CHECK_BOX) { @Override public void run() { - FilterState.setState(Severity.GROUP_BY_STATE_NAME); + FilterState.setGroupingMode(GroupingMode.STATE_NAME); pluginEventBus.post(new PluginListenerDefinition(PluginListenerType.FILTER_CHANGED, DataProvider.getInstance().sortResults())); } }; - + groupByStateNameAction.setId(ActionName.GROUP_BY_STATE_NAME.name()); groupByStateNameAction.setChecked(FilterState.groupByStateName); 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..c37b254a 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,8 @@ 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.GroupingMode; import com.checkmarx.eclipse.enums.State; import com.checkmarx.eclipse.views.GlobalSettings; @@ -45,11 +46,11 @@ public static void loadFiltersFromSettings() { low = Boolean.parseBoolean(GlobalSettings.getFromPreferences(Severity.LOW.name(), "true")); info = Boolean.parseBoolean(GlobalSettings.getFromPreferences(Severity.INFO.name(), "true")); groupBySeverity = Boolean - .parseBoolean(GlobalSettings.getFromPreferences(Severity.GROUP_BY_SEVERITY.name(), "true")); + .parseBoolean(GlobalSettings.getFromPreferences(GroupingMode.SEVERITY.name(), "true")); groupByQueryName = Boolean - .parseBoolean(GlobalSettings.getFromPreferences(Severity.GROUP_BY_QUERY_NAME.name(), "false")); + .parseBoolean(GlobalSettings.getFromPreferences(GroupingMode.QUERY_NAME.name(), "false")); groupByStateName = Boolean - .parseBoolean(GlobalSettings.getFromPreferences(Severity.GROUP_BY_STATE_NAME.name(), "false")); + .parseBoolean(GlobalSettings.getFromPreferences(GroupingMode.STATE_NAME.name(), "false")); notExploitable = Boolean.parseBoolean(GlobalSettings.getFromPreferences("NOT_EXPLOITABLE", "false")); confirmed = Boolean.parseBoolean(GlobalSettings.getFromPreferences("CONFIRMED", "true")); @@ -63,81 +64,97 @@ public static void loadFiltersFromSettings() { } /** - * Change severity state + * Change severity state (only actual severity levels, not grouping modes) */ public static void setState(Severity severity) { + if (severity == null) { + return; + } switch (severity) { - case CRITICAL: - critical = !critical; - GlobalSettings.storeInPreferences(Severity.CRITICAL.name(), String.valueOf(critical)); - break; - case HIGH: - high = !high; - GlobalSettings.storeInPreferences(Severity.HIGH.name(), String.valueOf(high)); - break; - case MEDIUM: - medium = !medium; - GlobalSettings.storeInPreferences(Severity.MEDIUM.name(), String.valueOf(medium)); - break; - case LOW: - low = !low; - GlobalSettings.storeInPreferences(Severity.LOW.name(), String.valueOf(low)); - break; - case INFO: - info = !info; - GlobalSettings.storeInPreferences(Severity.INFO.name(), String.valueOf(info)); - break; - case GROUP_BY_SEVERITY: - groupBySeverity = !groupBySeverity; - GlobalSettings.storeInPreferences(Severity.GROUP_BY_SEVERITY.name(), String.valueOf(groupBySeverity)); - break; - case GROUP_BY_QUERY_NAME: - groupByQueryName = !groupByQueryName; - GlobalSettings.storeInPreferences(Severity.GROUP_BY_QUERY_NAME.name(), String.valueOf(groupByQueryName)); - break; - case GROUP_BY_STATE_NAME: - groupByStateName = !groupByStateName; - GlobalSettings.storeInPreferences(Severity.GROUP_BY_STATE_NAME.name(), String.valueOf(groupByStateName)); - break; - default: - break; + case CRITICAL: + critical = !critical; + GlobalSettings.storeInPreferences(Severity.CRITICAL.name(), String.valueOf(critical)); + break; + case HIGH: + high = !high; + GlobalSettings.storeInPreferences(Severity.HIGH.name(), String.valueOf(high)); + break; + case MEDIUM: + medium = !medium; + GlobalSettings.storeInPreferences(Severity.MEDIUM.name(), String.valueOf(medium)); + break; + case LOW: + low = !low; + GlobalSettings.storeInPreferences(Severity.LOW.name(), String.valueOf(low)); + break; + case INFO: + info = !info; + GlobalSettings.storeInPreferences(Severity.INFO.name(), String.valueOf(info)); + break; + default: + break; + } + } + + /** + * Change grouping mode state (Severity, Query Name, or State Name) + */ + public static void setGroupingMode(GroupingMode mode) { + if (mode == null) { + return; + } + switch (mode) { + case SEVERITY: + groupBySeverity = !groupBySeverity; + GlobalSettings.storeInPreferences(GroupingMode.SEVERITY.name(), String.valueOf(groupBySeverity)); + break; + case QUERY_NAME: + groupByQueryName = !groupByQueryName; + GlobalSettings.storeInPreferences(GroupingMode.QUERY_NAME.name(), String.valueOf(groupByQueryName)); + break; + case STATE_NAME: + groupByStateName = !groupByStateName; + GlobalSettings.storeInPreferences(GroupingMode.STATE_NAME.name(), String.valueOf(groupByStateName)); + break; + default: + break; } } public static void setFilterState(State state) { switch (state.getName()) { - case "NOT_EXPLOITABLE": - notExploitable = !notExploitable; - GlobalSettings.storeInPreferences("NOT_EXPLOITABLE", String.valueOf(notExploitable)); - break; - case "PROPOSED_NOT_EXPLOITABLE": - proposedNotExploitable = !proposedNotExploitable; - GlobalSettings.storeInPreferences("PROPOSED_NOT_EXPLOITABLE", String.valueOf(proposedNotExploitable)); - break; - case "URGENT": - urgent = !urgent; - GlobalSettings.storeInPreferences("URGENT", String.valueOf(urgent)); - break; - case "IGNORED": - ignored = !ignored; - GlobalSettings.storeInPreferences("IGNORED", String.valueOf(ignored)); - break; - case "CONFIRMED": - confirmed = !confirmed; - GlobalSettings.storeInPreferences("CONFIRMED", String.valueOf(confirmed)); - break; - case "NOT_IGNORED": - not_ignored = !not_ignored; - GlobalSettings.storeInPreferences("NOT_IGNORED", String.valueOf(not_ignored)); - break; - case "TO_VERIFY": - to_verify = !to_verify; - GlobalSettings.storeInPreferences("TO_VERIFY", String.valueOf(to_verify)); - break; - default: - // For custom states, toggle the global customState flag - setCustomStateFilter(); - break; + case "NOT_EXPLOITABLE": + notExploitable = !notExploitable; + GlobalSettings.storeInPreferences("NOT_EXPLOITABLE", String.valueOf(notExploitable)); + break; + case "PROPOSED_NOT_EXPLOITABLE": + proposedNotExploitable = !proposedNotExploitable; + GlobalSettings.storeInPreferences("PROPOSED_NOT_EXPLOITABLE", String.valueOf(proposedNotExploitable)); + break; + case "URGENT": + urgent = !urgent; + GlobalSettings.storeInPreferences("URGENT", String.valueOf(urgent)); + break; + case "IGNORED": + ignored = !ignored; + GlobalSettings.storeInPreferences("IGNORED", String.valueOf(ignored)); + break; + case "CONFIRMED": + confirmed = !confirmed; + GlobalSettings.storeInPreferences("CONFIRMED", String.valueOf(confirmed)); + break; + case "NOT_IGNORED": + not_ignored = !not_ignored; + GlobalSettings.storeInPreferences("NOT_IGNORED", String.valueOf(not_ignored)); + break; + case "TO_VERIFY": + to_verify = !to_verify; + GlobalSettings.storeInPreferences("TO_VERIFY", String.valueOf(to_verify)); + break; + default: + // For custom states, toggle the global customState flag + setCustomStateFilter(); + break; } } @@ -160,22 +177,22 @@ public static boolean isFilterStateEnabled(String state) { String normalized = state.trim().toUpperCase(); if (PREDEFINED_STATE_SET.contains(normalized)) { switch (normalized) { - case "NOT_EXPLOITABLE": - return notExploitable; - case "PROPOSED_NOT_EXPLOITABLE": - return proposedNotExploitable; - case "TO_VERIFY": - return to_verify; - case "CONFIRMED": - return confirmed; - case "URGENT": - return urgent; - case "NOT_IGNORED": - return not_ignored; - case "IGNORED": - return ignored; - default: - break; + case "NOT_EXPLOITABLE": + return notExploitable; + case "PROPOSED_NOT_EXPLOITABLE": + return proposedNotExploitable; + case "TO_VERIFY": + return to_verify; + case "CONFIRMED": + return confirmed; + case "URGENT": + return urgent; + case "NOT_IGNORED": + return not_ignored; + case "IGNORED": + return ignored; + default: + break; } } else { // [AST-92100] Not a predefined state, check if this custom state is enabled @@ -185,32 +202,53 @@ public static boolean isFilterStateEnabled(String state) { } /** - * Checks whether a severity is enabled + * Checks whether a severity is enabled (only actual severity levels) */ public static boolean isSeverityEnabled(String severity) { - switch (Severity.getSeverity(severity)) { - case CRITICAL: - return critical; - case HIGH: - return high; - case MEDIUM: - return medium; - case LOW: - return low; - case INFO: - return info; - case GROUP_BY_SEVERITY: - return groupBySeverity; - case GROUP_BY_QUERY_NAME: - return groupByQueryName; - case GROUP_BY_STATE_NAME: - return groupByStateName; - default: - break; + if (severity == null) { + return false; + } + try { + switch (Severity.getSeverity(severity)) { + case CRITICAL: + return critical; + case HIGH: + return high; + case MEDIUM: + return medium; + case LOW: + return low; + case INFO: + return info; + default: + break; + } + } catch (IllegalArgumentException e) { + // Invalid severity string + return false; } return false; } + /** + * Checks whether a grouping mode is enabled + */ + public static boolean isGroupingModeEnabled(GroupingMode mode) { + if (mode == null) { + return false; + } + switch (mode) { + case SEVERITY: + return groupBySeverity; + case QUERY_NAME: + return groupByQueryName; + case STATE_NAME: + return groupByStateName; + default: + return false; + } + } + /** * Reset filters state */ 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..83424dc7 --- /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..0bb9fd72 --- /dev/null +++ b/common-lib/META-INF/MANIFEST.MF @@ -0,0 +1,62 @@ +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.27.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.ast.asca, + com.checkmarx.ast.codebashing, + com.checkmarx.ast.containersrealtime, + com.checkmarx.ast.iacrealtime, + com.checkmarx.ast.kicsRealtimeResults, + com.checkmarx.ast.kicsRealtimeResults.ast.kicsRealtimeResult, + com.checkmarx.ast.learnMore, + com.checkmarx.ast.mask, + com.checkmarx.ast.ossrealtime, + com.checkmarx.ast.predicate, + com.checkmarx.ast.project, + com.checkmarx.ast.realtime, + com.checkmarx.ast.remediation, + com.checkmarx.ast.results, + com.checkmarx.ast.results.result, + com.checkmarx.ast.scan, + com.checkmarx.ast.secretsrealtime, + com.checkmarx.ast.tenant, + com.checkmarx.ast.utils, + com.checkmarx.ast.wrapper, + 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, + com.checkmarx.eclipse.common.wrapper, + com.fasterxml.jackson.annotation, + com.fasterxml.jackson.core, + com.fasterxml.jackson.core.type, + com.fasterxml.jackson.databind, + 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, + org.slf4j diff --git a/common-lib/build.properties b/common-lib/build.properties new file mode 100644 index 00000000..a1599af2 --- /dev/null +++ b/common-lib/build.properties @@ -0,0 +1,12 @@ +output.. = bin/ +bin.includes = META-INF/,\ + lib/ast-cli-java-wrapper-2.4.27.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/lib/ast-cli-java-wrapper-2.4.27.jar b/common-lib/lib/ast-cli-java-wrapper-2.4.27.jar new file mode 100644 index 00000000..3df6ef7a --- /dev/null +++ b/common-lib/lib/ast-cli-java-wrapper-2.4.27.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5313792421835ddfd332713da41a82eb8234705c981bdbdd1431d797538185c0 +size 140401594 diff --git a/common-lib/lib/commons-lang3-3.18.0.jar b/common-lib/lib/commons-lang3-3.18.0.jar new file mode 100644 index 00000000..b3a4eb8a --- /dev/null +++ b/common-lib/lib/commons-lang3-3.18.0.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eeeae8d20c078abb64b015ec158add383ac581571cddc45c68f0c9ae0230720 +size 702952 diff --git a/common-lib/lib/jackson-annotations-2.21.jar b/common-lib/lib/jackson-annotations-2.21.jar new file mode 100644 index 00000000..7a137fe2 --- /dev/null +++ b/common-lib/lib/jackson-annotations-2.21.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b +size 82104 diff --git a/common-lib/lib/jackson-core-2.21.4.jar b/common-lib/lib/jackson-core-2.21.4.jar new file mode 100644 index 00000000..d43b85b8 --- /dev/null +++ b/common-lib/lib/jackson-core-2.21.4.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b40a06396f239f8de2da57419adde6e94e5edc18a2171d471ea05eeed4e5c2d +size 594183 diff --git a/common-lib/lib/jackson-databind-2.21.5.jar b/common-lib/lib/jackson-databind-2.21.5.jar new file mode 100644 index 00000000..188a54f7 --- /dev/null +++ b/common-lib/lib/jackson-databind-2.21.5.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:507418c0fafd38b2b2cfb704521630da613a8e4cc8381195a6f418017883e2c0 +size 1705906 diff --git a/common-lib/lib/slf4j-api-2.0.17.jar b/common-lib/lib/slf4j-api-2.0.17.jar new file mode 100644 index 00000000..181ddf15 --- /dev/null +++ b/common-lib/lib/slf4j-api-2.0.17.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832 +size 69908 diff --git a/common-lib/lib/slf4j-reload4j-2.0.17.jar b/common-lib/lib/slf4j-reload4j-2.0.17.jar new file mode 100644 index 00000000..07eb8d6c --- /dev/null +++ b/common-lib/lib/slf4j-reload4j-2.0.17.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62dfd069563dbce91d276c9a38d557da49df7be15b03bb898b63b5390c6bb889 +size 10734 diff --git a/common-lib/lib/slf4j-simple-2.0.17.jar b/common-lib/lib/slf4j-simple-2.0.17.jar new file mode 100644 index 00000000..00965c2b --- /dev/null +++ b/common-lib/lib/slf4j-simple-2.0.17.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddfea59ac074c6d3e24ac2c38622d2d963895e17f70b38ed4bdae4d780be6964 +size 15716 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..79a19f46 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java @@ -0,0 +1,22 @@ +package com.checkmarx.eclipse.common.enums; + +/** + * Severity levels for security findings. + * + * Note: UI grouping modes are kept separate in the plugin module (GroupingMode + * enum). + * This enum is limited to actual severity levels for the shared contract. + */ +public enum Severity { + + CRITICAL, + HIGH, + MEDIUM, + LOW, + INFO, + MALICIOUS; + + 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/IMcpInstallCallback.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java new file mode 100644 index 00000000..26915cab --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java @@ -0,0 +1,31 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Receives the outcome of an MCP install triggered from the UI (e.g. the "Install + * MCP" link on CheckmarxPreferencePage). Unlike the silent, best-effort auto-install + * run at startup, a user-initiated install needs to report back whether it actually + * succeeded so the UI can show a result message. + * + *

    May be invoked from a background thread - implementations that touch SWT + * widgets must marshal onto the display thread themselves. + */ +public interface IMcpInstallCallback { + + /** + * Called when the MCP configuration was installed/updated successfully. + */ + void onSuccess(); + + /** + * Called when the install ran successfully but there was nothing to change - the + * server entry already matches the current API key/URL exactly. + */ + void onAlreadyUpToDate(); + + /** + * Called when the install could not be completed. + * + * @param errorMessage a user-presentable reason for the failure + */ + void onFailure(String errorMessage); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java new file mode 100644 index 00000000..13f34554 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java @@ -0,0 +1,19 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Service for installing the Checkmarx MCP server configuration. + * + * Allows preference pages in common-lib (e.g. CheckmarxPreferencePage) to trigger + * MCP installation without depending on devassist-lib, which owns the actual + * McpInstallService implementation. + */ +public interface IMcpInstallHandler { + + /** + * Installs/updates the Checkmarx MCP server configuration for the currently + * authenticated user, reporting the outcome to the given callback. + * + * @param callback notified of success or failure, possibly from a background thread + */ + void installMcp(IMcpInstallCallback callback); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallCallback.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallCallback.java new file mode 100644 index 00000000..a07cdba7 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallCallback.java @@ -0,0 +1,29 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Receives the outcome of an MCP uninstall triggered after logout. + * + *

    May be invoked from a background thread - implementations that touch SWT + * widgets must marshal onto the display thread themselves. + */ +public interface IMcpUninstallCallback { + + /** + * Called when the MCP configuration was uninstalled/removed successfully. + */ + void onSuccess(); + + /** + * Called when no MCP configuration entry was found to uninstall - the uninstall + * operation succeeded, but there was nothing to remove (already uninstalled, + * or never installed in the first place). + */ + void onNotFound(); + + /** + * Called when the uninstall could not be completed. + * + * @param errorMessage a user-presentable reason for the failure + */ + void onFailure(String errorMessage); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallHandler.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallHandler.java new file mode 100644 index 00000000..503f00a4 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpUninstallHandler.java @@ -0,0 +1,18 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Service for uninstalling the Checkmarx MCP server configuration. + * + * Allows preference pages in common-lib (e.g. PreferencesPage) to trigger + * MCP uninstallation on logout without depending on devassist-lib, which owns the actual + * McpInstallService implementation. + */ +public interface IMcpUninstallHandler { + + /** + * Uninstalls/removes the Checkmarx MCP server configuration after logout. + * + * @param callback notified of success or failure, possibly from a background thread + */ + void uninstallMcp(IMcpUninstallCallback callback); +} 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..7440bb3d --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java @@ -0,0 +1,473 @@ +package com.checkmarx.eclipse.common.preferences; + +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.util.PropertyChangeEvent; +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.utils.PluginConstants; +import com.checkmarx.eclipse.common.listener.IMcpInstallCallback; +import com.checkmarx.eclipse.common.listener.IMcpInstallHandler; +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 Label mcpStatusLabel; + private boolean loggedIn; + + public CheckmarxPreferencePage() { + super(); + setPreferenceStore(com.checkmarx.eclipse.common.preferences.Preferences.STORE); + // Listen for preference changes to update login state. + // Critical: if user logs out in another page while this page is visible in the + // same + // dialog session, we need to refresh the UI to show logged-out content instead + // of stale + // logged-in checkboxes. Without this, performOk() would still run with stale + // loggedIn=true. + Preferences.STORE.addPropertyChangeListener(this::handlePreferenceChange); + } + + /** + * Called when preferences change (e.g., user logs out in another page of the + * same dialog). + * Re-reads the login state and updates the visible UI accordingly. + */ + private void handlePreferenceChange(PropertyChangeEvent event) { + // Re-check login state: if API key was cleared, we need to switch from + // logged-in scanner checkboxes to logged-out message + boolean isNowLoggedIn = Preferences.isAuthenticated(); + if (loggedIn != isNowLoggedIn) { + loggedIn = isNowLoggedIn; + } + } + + @Override + protected Control createContents(Composite parent) { + loggedIn = Preferences.isAuthenticated(); + 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, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); + Composite ascaComp = createIndentComposite(mainPanel); + ascaCheckbox = new Button(ascaComp, SWT.CHECK); + ascaCheckbox.setText(PluginConstants.ASCA_CHECKBOX); + + // --- OSS Section --- + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); + Composite ossComp = createIndentComposite(mainPanel); + ossCheckbox = new Button(ossComp, SWT.CHECK); + ossCheckbox.setText(PluginConstants.OSS_REALTIME_CHECKBOX); + + // --- Secrets Section --- + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); + Composite secretsComp = createIndentComposite(mainPanel); + secretsCheckbox = new Button(secretsComp, SWT.CHECK); + secretsCheckbox.setText(PluginConstants.SECRETS_REALTIME_CHECKBOX); + + // --- Containers Section --- + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); + Composite containersComp = createIndentComposite(mainPanel); + containersCheckbox = new Button(containersComp, SWT.CHECK); + containersCheckbox.setText(PluginConstants.CONTAINERS_REALTIME_CHECKBOX); + + // --- IaC Section --- + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); + Composite iacComp = createIndentComposite(mainPanel); + iacCheckbox = new Button(iacComp, SWT.CHECK); + iacCheckbox.setText(PluginConstants.IAC_REALTIME_CHECKBOX); + + // --- Container Tool Selection Section --- + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); + Composite containerToolComp = createIndentComposite(mainPanel); + Label containerDesc = new Label(containerToolComp, SWT.WRAP); + containerDesc.setText(PluginConstants.CONTAINERS_TOOL_DESCRIPTION); + GridData descData = new GridData(GridData.FILL_HORIZONTAL); + containerDesc.setLayoutData(descData); + + containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); + containersToolCombo.setItems(PluginConstants.CONTAINERS_TOOLS); + containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + // --- Checkmarx MCP Section --- + // A horizontal rule marks this as a distinct settings group, separate from the + // Realtime Scanner sections above. + Label mcpSeparator = new Label(mainPanel, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData mcpSeparatorData = new GridData(GridData.FILL_HORIZONTAL); + mcpSeparatorData.verticalIndent = 6; + mcpSeparator.setLayoutData(mcpSeparatorData); + + createSectionHeader(mainPanel, PluginConstants.CHECKMARX_MCP_SECTION_TITLE); + Composite mcpComp = createIndentComposite(mainPanel); + + Label mcpDesc = new Label(mcpComp, SWT.WRAP); + mcpDesc.setText(PluginConstants.MCP_DESCRIPTION); + mcpDesc.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // installMcpLink and mcpStatusLabel share a row so the result message appears + // right next to the link that triggered it, rather than on its own line. + Composite installMcpRow = new Composite(mcpComp, SWT.NONE); + GridLayout installMcpRowLayout = new GridLayout(2, false); + installMcpRowLayout.marginWidth = 0; + installMcpRowLayout.marginHeight = 0; + installMcpRowLayout.horizontalSpacing = 10; + installMcpRow.setLayout(installMcpRowLayout); + installMcpRow.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Link installMcpLink = new Link(installMcpRow, SWT.NONE); + installMcpLink.setText("" + PluginConstants.INSTALL_MCP_LINK_TEXT + ""); + installMcpLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false)); + + mcpStatusLabel = new Label(installMcpRow, SWT.WRAP); + mcpStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + installMcpLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + installMcp(); + } + }); + + Link editMcpSettingsLink = new Link(mcpComp, SWT.NONE); + editMcpSettingsLink.setText("" + PluginConstants.EDIT_MCP_SETTINGS_LINK_TEXT + ""); + editMcpSettingsLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + editMcpSettingsLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + editMcpSettings(); + } + }); + + loadValues(); + return mainPanel; + } + + /** + * Installs/updates the Checkmarx MCP server configuration. Delegates to the handler + * registered by devassist-lib (this bundle - common-lib - doesn't depend on it directly), + * and shows the result right next to the "Install MCP" link. + */ + private void installMcp() { + IMcpInstallHandler handler = Preferences.getMcpInstallHandler(); + if (handler == null) { + CxLogger.warning("[PREFS] MCP install requested before the handler was registered"); + showMcpStatus(false, PluginConstants.MCP_INSTALL_UNAVAILABLE_MESSAGE); + return; + } + + showMcpStatus(null, PluginConstants.MCP_INSTALLING_STATE); + + handler.installMcp(new IMcpInstallCallback() { + @Override + public void onSuccess() { + Display.getDefault().asyncExec(() -> showMcpStatus(true, PluginConstants.MCP_INSTALL_SUCCESS_MESSAGE)); + } + + @Override + public void onAlreadyUpToDate() { + Display.getDefault().asyncExec(() -> showMcpStatus(true, PluginConstants.MCP_ALREADY_UP_TO_DATE_MESSAGE)); + } + + @Override + public void onFailure(String errorMessage) { + Display.getDefault().asyncExec(() -> showMcpStatus(false, errorMessage)); + } + }); + } + + /** + * Updates mcpStatusLabel with an install result/progress message. + * + * @param success true = success (green), false = failure (red), null = in-progress + * (default color) + */ + private void showMcpStatus(Boolean success, String message) { + if (mcpStatusLabel == null || mcpStatusLabel.isDisposed()) { + return; + } + + Display display = mcpStatusLabel.getDisplay(); + if (success == null) { + mcpStatusLabel.setForeground(null); + } else if (success) { + mcpStatusLabel.setForeground(display.getSystemColor(SWT.COLOR_DARK_GREEN)); + } else { + mcpStatusLabel.setForeground(display.getSystemColor(SWT.COLOR_RED)); + } + mcpStatusLabel.setText(message); + mcpStatusLabel.getParent().layout(true, true); + } + + /** + * Opens GitHub Copilot for Eclipse's own MCP preference page, where the Checkmarx MCP + * server entry (once installed) can be reviewed/edited alongside any other MCP servers. + */ + private void editMcpSettings() { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(getShell(), + PluginConstants.COPILOT_MCP_PREFERENCE_PAGE_ID, null, null); + if (dialog != null) { + dialog.open(); + } + } + + /** + * 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(PluginConstants.LOGIN_NOTE_CXONE_ASSIST); + message.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Link goToLoginLink = new Link(composite, SWT.NONE); + goToLoginLink.setText(""+PluginConstants.GO_TO_CHECKMARX_ONE+""); + 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() { + // Check credentials fresh, not from captured field. + // If user logged out while viewing another page, loggedIn would be stale. + boolean isCurrentlyLoggedIn = Preferences.isAuthenticated(); + if (!isCurrentlyLoggedIn) { + 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() { + // Check credentials fresh, not from captured field. + // Critical: if user logged out while viewing another page within the same + // dialog session, + // loggedIn would be stale and we'd save/notify with false authentication + // status. + boolean isCurrentlyLoggedIn = Preferences.isAuthenticated(); + if (!isCurrentlyLoggedIn) { + return super.performOk(); + } + IPreferenceStore store = getPreferenceStore(); + + // Get current UI selections. It's possible this page's controls were never + // created (Eclipse may call performOk() on pages that haven't had + // createContents() invoked), or the controls may have been disposed. In + // that case fall back to the stored preference values instead of + // dereferencing null controls which caused an NPE in the field. + boolean ascaSelected = store.getBoolean(PREF_ASCA_ENABLED); + if (ascaCheckbox != null && !ascaCheckbox.isDisposed()) { + ascaSelected = ascaCheckbox.getSelection(); + } + + boolean ossSelected = store.getBoolean(PREF_OSS_ENABLED); + if (ossCheckbox != null && !ossCheckbox.isDisposed()) { + ossSelected = ossCheckbox.getSelection(); + } + + boolean secretsSelected = store.getBoolean(PREF_SECRETS_ENABLED); + if (secretsCheckbox != null && !secretsCheckbox.isDisposed()) { + secretsSelected = secretsCheckbox.getSelection(); + } + + boolean containersSelected = store.getBoolean(PREF_CONTAINERS_ENABLED); + if (containersCheckbox != null && !containersCheckbox.isDisposed()) { + containersSelected = containersCheckbox.getSelection(); + } + + boolean iacSelected = store.getBoolean(PREF_IAC_ENABLED); + if (iacCheckbox != null && !iacCheckbox.isDisposed()) { + iacSelected = iacCheckbox.getSelection(); + } + + String containersTool = store.getString(PREF_CONTAINERS_TOOL); + if (containersToolCombo != null && !containersToolCombo.isDisposed()) { + try { + String text = containersToolCombo.getText(); + if (text != null && !text.isBlank()) { + containersTool = text; + } + } catch (Exception ex) { + // Defensive: protect against any SWT oddities; fall back to store value + CxLogger.warning("[PREFS-PAGE] Failed to read containersToolCombo text, using stored value: " + ex.getMessage()); + } + } + + // 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..293b9497 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java @@ -0,0 +1,218 @@ +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.IMcpInstallHandler; +import com.checkmarx.eclipse.common.listener.IMcpUninstallHandler; +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; + + // Handler for installing the Checkmarx MCP server configuration (registered by devassist-lib) + private static IMcpInstallHandler mcpInstallHandler; + + // Handler for uninstalling the Checkmarx MCP server configuration (registered by devassist-lib) + private static IMcpUninstallHandler mcpUninstallHandler; + + 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 boolean isCredentialsValidated() { + return STORE.getBoolean(CREDENTIALS_VALIDATED); + } + + public static void setCredentialsValidated(boolean validated) { + STORE.setValue(CREDENTIALS_VALIDATED, validated); + } + + /** + * Single source of truth for "is the user logged in", independent of which credential + * type produced that state. Callers across the plugin should check this - not API key + * presence - so that a future auth method (e.g. OAuth) only needs to set/clear this same + * flag to plug into every existing authenticated-only code path. + */ + public static boolean isAuthenticated() { + return isCredentialsValidated(); + } + + 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; + } + + public static void setMcpInstallHandler(IMcpInstallHandler handler) { + mcpInstallHandler = handler; + } + + public static IMcpInstallHandler getMcpInstallHandler() { + return mcpInstallHandler; + } + + public static void setMcpUninstallHandler(IMcpUninstallHandler handler) { + mcpUninstallHandler = handler; + } + + public static IMcpUninstallHandler getMcpUninstallHandler() { + return mcpUninstallHandler; + } + + // ============================================================================ + // 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..d272318e --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -0,0 +1,567 @@ +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.dialogs.MessageDialog; +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.IMcpUninstallCallback; +import com.checkmarx.eclipse.common.listener.IMcpUninstallHandler; +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; + +/** + * PreferencesPage class for Chekmarx One Preference Page (Login settings) + */ +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 One Assist" + * (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 = Preferences.isAuthenticated(); + + 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(PluginConstants.CHECKMARX_ONE); + } + + @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); + + // helpLink lives in its own composite, isolated from the fields below, so its own + // sizing/margins can never influence the spacing between the API key / additional params labels and their input boxes. + Composite helpComposite = new Composite(topComposite, SWT.NONE); + GridLayout helpLayout = new GridLayout(); + helpLayout.numColumns = 1; + helpLayout.marginHeight = 0; + helpLayout.marginWidth = 0; + helpComposite.setLayout(helpLayout); + helpComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Link helpLink = new Link(helpComposite, SWT.NONE); + helpLink.setText("" + + PluginConstants.PREFERENCES_HELP_LINK_TEXT + ""); + helpLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + helpLink.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 Checkmarx One Eclipse Plugin Help Page link.", e1); + e1.printStackTrace(); + } + } + }); + + spacer(topComposite); + + // apiKey and additionalParams get their own composite with a standard, fixed + // label-to-input gap - kept separate from topComposite (and from helpComposite above) + // so nothing else on the page can stretch or shrink that gap. + Composite fieldsComposite = new Composite(topComposite, SWT.NONE); + GridLayout fieldsLayout = new GridLayout(); + // Use 2 columns so each FieldEditor places its label in column 1 and the + // input control in column 2. This allows us to set a widthHint on the + // input control without the control stretching to the full dialog width. + fieldsLayout.numColumns = 2; + fieldsLayout.marginHeight = 0; + fieldsLayout.marginWidth = 0; + fieldsLayout.verticalSpacing = 4; + fieldsComposite.setLayout(fieldsLayout); + fieldsComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, fieldsComposite); + apiKeyField = apiKey; + addField(apiKey); + Text textControl = apiKey.getTextControl(fieldsComposite); + textControl.setEchoChar('*'); + + StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, + PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, + StringFieldEditor.VALIDATE_ON_KEY_STROKE, fieldsComposite); + 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(); + + + boolean isConnected = (Preferences.isAuthenticated() && StringUtils.isNotBlank(Preferences.getApiKey())); + + // Locked while connected, so the validated key can't be edited out from under the + // "connected" state - re-enabled on logout. + textControl.setEnabled(!isConnected); + + // 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); + + Link cliHelp = new Link(topComposite, SWT.NONE); + cliHelp.setText("" + + PluginConstants.PREFERENCES_CLI_HELP_LINK_TEXT + ""); + cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); + 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(); + } + } + }); + + spacer(topComposite); + + // 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]; + + Composite buttonsComposite = new Composite(topComposite, SWT.NONE); + GridLayout buttonsLayout = new GridLayout(); + buttonsLayout.numColumns = 2; + buttonsLayout.marginHeight = 0; + buttonsLayout.marginWidth = 0; + buttonsLayout.horizontalSpacing = 10; + buttonsComposite.setLayout(buttonsLayout); + buttonsComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + + // Give both buttons a fixed minimum width so they aren't sized to hug their + // text - + // without this, "Logout" ends up noticeably narrower than "Connect to + // Checkmarx". + final int buttonWidthHint = 140; + + Button connectionButton = new Button(buttonsComposite, SWT.PUSH); + connectionButton.setText(PluginConstants.CONNECT_TO_CHECKMARX); + GridData connectionButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); + connectionButtonGridData.widthHint = buttonWidthHint; + connectionButton.setLayoutData(connectionButtonGridData); + // Disabled while already connected - re-enabled on logout (see logoutButton below). + connectionButton.setEnabled(!isConnected); + + // connectionLabel (the "Validating.../Connected" status text) is created after + // buttonsComposite so it renders below the Connect/Logout buttons, per + // AUTH_SUCCESS_DISPLAY + // placement - it's declared here, before the listeners below that reference it. + spacer(topComposite); + + Label connectionLabel = new Label(topComposite, SWT.WRAP); + connectionLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + if (isConnected) { + connectionLabel.setText(PluginConstants.AUTH_SUCCESS_DISPLAY); + setStatusLabelColor(connectionLabel, true); + connectionButton.setEnabled(false); + + } + + textControl.addModifyListener(e -> { + boolean hasApiKey = StringUtils.isNotBlank(textControl.getText()); + // Fetch live authentication status rather than relying on the static isConnected closure variable + boolean currentlyConnected = Preferences.isAuthenticated() && StringUtils.isNotBlank(Preferences.getApiKey()); + // API key is mandatory to enable Connect; also disable Connect if the key + // matches the already-validated key (it stays connected in that case). + connectionButton.setEnabled(currentlyConnected ? false : hasApiKey); + }); + connectionButton.addSelectionListener(new SelectionAdapter() { + + public void widgetSelected(SelectionEvent e) { + + String apiKey_str = apiKey.getStringValue(); + + // API key is mandatory — don't attempt authentication without it. + + if (StringUtils.isBlank(apiKey_str)) { + MessageDialog.openWarning(getShell(), "Missing API Key", + "Please enter an API key before attempting to connect."); + // Ensure Connect remains disabled until user enters a key + connectionButton.setEnabled(false); + return; + } + + String additionalParams_str = additionalParams.getStringValue(); + connectionButton.setEnabled(false); + connectionLabel.setText(PluginConstants.PREFERENCES_VALIDATING_STATE); + setStatusLabelColor(connectionLabel, null); + 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(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, t.getMessage()), 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. + + // 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); + // connectionButton stays disabled - it's only re-enabled on logout, or + // below if this attempt actually failed. + if (!textControl.isDisposed()) { + textControl.setEnabled(false); + } + 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(); + } + + checkMCPStatus(logoutButtonHolder, connectionLabel, apiKey_str, additionalParams_str, result); + } else { + // Authentication failed - the flow ends here with no welcome dialog, + // so show the failure message right away, restore Logout, and let the + // user retry the connect. + if (!connectionButton.isDisposed()) { + connectionButton.setEnabled(true); + } + if (!connectionLabel.isDisposed()) { + connectionLabel.setText(mapAuthResult(result)); + setStatusLabelColor(connectionLabel, false); + } + if (!getFieldEditorParent().isDisposed()) { + getFieldEditorParent().layout(); + } + if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { + logoutButtonHolder[0].setEnabled(false); + } + } + })); + } + }); + + Button logoutButton = new Button(buttonsComposite, SWT.PUSH); + logoutButtonHolder[0] = logoutButton; + logoutButton.setText(PluginConstants.LOGOUT); + GridData logoutButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); + logoutButtonGridData.widthHint = 80; + logoutButton.setLayoutData(logoutButtonGridData); + // Nothing to log out of until connected - mirrors connectionButton's inverse state. + logoutButton.setEnabled(isConnected); + logoutButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + MessageDialog confirmDialog = new MessageDialog(getShell(), PluginConstants.LOGOUT_CONFIRM_TITLE, null, + PluginConstants.LOGOUT_CONFIRM_MESSAGE, MessageDialog.QUESTION, + new String[] { "Yes", "Cancel" }, 0); + if (confirmDialog.open() != 0) { + return; + } + + // Only mark the credentials as no longer validated - the API key itself stays + // stored and visible in the field. Every "am I logged in" check in the plugin + // now goes through Preferences.isAuthenticated() (not "API key non-blank"), so + // leaving the key in place here no longer makes any of them think the user is + // still logged in. + Preferences.setCredentialsValidated(false); + connectionButton.setEnabled(true); + textControl.setEnabled(true); + logoutButton.setEnabled(false); + connectionLabel.setText(PluginConstants.LOGOUT_SUCCESS_MESSAGE); + setStatusLabelColor(connectionLabel, true); + 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(); + } + + uninstallMCP(); + } + }); + + spacer(topComposite); + spacer(topComposite); + + realtimeScannersLink = new Link(topComposite, SWT.NONE); + realtimeScannersLink.setText("" + PluginConstants.GO_TO_CHECKMARX_ONE_ASSIST + ""); + 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(); + } + } + }); + + // Deferred via asyncExec - the dialog's shell isn't shown/realized yet at this point + // in createFieldEditors(), so an immediate setFocus() here would be ignored. + Display.getDefault().asyncExec(() -> { + if (!textControl.isDisposed()) { + textControl.setFocus(); + } + }); + } + + private static String mapAuthResult(String result) { + if (result != null && result.contains(PluginConstants.AUTH_SUCCESS_PATTERN)) { + return PluginConstants.AUTH_SUCCESS_DISPLAY; + } + // Log the actual failure reason (invalid key, network error, tenant misconfiguration, + // etc.) for diagnosis, but always show the user the same fixed message - the raw + // reason isn't reliably meaningful/actionable to them and may leak backend details. + CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, result), new Exception(result)); + return PluginConstants.AUTH_FAILURE_DISPLAY; + } + + private Label spacer(Composite parent) { + return new Label(parent, SWT.NONE); + } + + /** + * Colors the login/logout status label: green for a success message (connected, + * logged out), red for a failure message, or the default color while a message is + * neutral (e.g. "Validating..."). + */ + private void setStatusLabelColor(Label label, Boolean success) { + if (label == null || label.isDisposed()) { + return; + } + Display display = label.getDisplay(); + if (success == null) { + label.setForeground(null); + } else if (success) { + label.setForeground(display.getSystemColor(SWT.COLOR_DARK_GREEN)); + } else { + label.setForeground(display.getSystemColor(SWT.COLOR_RED)); + } + } + + @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; + } + + /** + * Checks the MCP (Checkmarx One Assist) status from the server asynchronously and updates the UI accordingly. + * @param logoutButtonHolder + * @param connectionLabel + * @param apiKey_str + * @param additionalParams_str + * @param result + */ + private void checkMCPStatus(final Button[] logoutButtonHolder, Label connectionLabel, String apiKey_str, + String additionalParams_str, String result) { + // 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)); + setStatusLabelColor(connectionLabel, true); + } + 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); + } + } + })); + } + + /** + * Uninstalls the Checkmarx MCP configuration after a successful logout. + * Delegates to the handler registered by devassist-lib. + */ + private void uninstallMCP() { + IMcpUninstallHandler handler = Preferences.getMcpUninstallHandler(); + if (handler == null) { + CxLogger.info("[PREFS] MCP uninstall handler not registered - skipping MCP uninstall"); + return; + } + + CxLogger.info("[PREFS] Triggering MCP uninstall after logout..."); + handler.uninstallMcp(new IMcpUninstallCallback() { + @Override + public void onSuccess() { + CxLogger.info("[PREFS] ✓ Checkmarx MCP uninstalled successfully from Copilot preferences."); + } + + @Override + public void onNotFound() { + CxLogger.info("[PREFS] No Checkmarx MCP configuration entry found to uninstall (may not have been installed)."); + } + + @Override + public void onFailure(String errorMessage) { + CxLogger.error("[PREFS] ✗ Failed to uninstall Checkmarx MCP: " + errorMessage, new Exception(errorMessage)); + } + }); + } +} \ No newline at end of file diff --git a/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java new file mode 100644 index 00000000..d9a9f7b8 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java @@ -0,0 +1,26 @@ +package com.checkmarx.eclipse.common.runner; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; + +public class Authenticator { + + private Authenticator() { + // Private constructor to prevent instantiation + } + + protected static final String AUTH_STATUS = "Authentication Status: "; + public static final Authenticator INSTANCE = new Authenticator(); + + public String doAuthentication(String apiKey, String additionalParams) { + try { + String cxValidateOutput = new WrapperProvider().authValidate(apiKey, additionalParams); + CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, cxValidateOutput)); + return cxValidateOutput; + } catch (Exception e) { + CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, e.getMessage()), e); + return e.getMessage(); + } + } +} \ No newline at end of file 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..2ed6a381 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java @@ -0,0 +1,38 @@ +package com.checkmarx.eclipse.common.runner; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; + +/** + * Provides tenant-specific settings from the Checkmarx API. + * Fetches configuration details like MCP enablement status. + */ +public class TenantSettingsProvider { + private static final String LOG_PREFIX = "[TENANT_SETTINGS_PROVIDER] "; + 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 { + boolean mcpEnabled = new WrapperProvider().isAiMcpServerEnabled(apiKey, additionalParams); + CxLogger.info(String.format("MCP Server Status: %s", mcpEnabled ? "ENABLED" : "DISABLED")); + return mcpEnabled; + } catch (Exception e) { + CxLogger.error(String.format("%s Failed to check MCP server status: %s", LOG_PREFIX, 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 67% 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..2e53df17 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,6 +1,10 @@ -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 AGENT_NAME = "Eclipse"; + public static final String CHECKMARX_ONE = "Checkmarx One"; public static final String EMPTY_STRING = ""; public static final String SAST = "sast"; public static final String SCA_DEPENDENCY = "sca"; @@ -21,6 +25,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 ********************************/ @@ -50,7 +55,6 @@ public class PluginConstants { public static final String INFO_CHANGE_BRANCH_EVENT_NOT_TRIGGERED = "Change branch event not triggered. Branch already selected"; public static final String INFO_CHANGE_PROJECT_EVENT_NOT_TRIGGERED = "Change project event not triggered. Project already selected"; public static final String AUTH_SUCCESS_PATTERN = "Successfully authenticated"; - public static final String AUTH_SUCCESS_DISPLAY = "You are connected to Checkmarx One"; /******************************** TREE MESSAGES ********************************/ public static final String TREE_INVALID_SCAN_ID_FORMAT = "Invalid scan id format."; @@ -60,11 +64,22 @@ public class PluginConstants { /******************************** PREFERENCES ********************************/ public static final String PREFERENCES_API_KEY = "API key:"; public static final String PREFERENCES_ADDITIONAL_OPTIONS = "Additional Params:"; - public static final String PREFERENCES_TEST_CONNECTION = "Test Connection"; + public static final String CONNECT_TO_CHECKMARX = "Connect to Checkmarx"; + public static final String LOGOUT = "Logout"; + public static final String GO_TO_CHECKMARX_ONE_ASSIST = "Go to Checkmarx One Assist"; + public static final String PREFERENCES_HELP_LINK_TEXT = "Checkmarx One Eclipse Plugin Help Page"; + public static final String PREFERENCES_HELP_LINK_URL = "https://checkmarx.com/resource/documents/en/34965-68728-checkmarx-one-eclipse-plugin.html"; + public static final String PREFERENCES_CLI_HELP_LINK_TEXT = "CLI command that supports a set of global flags"; + public static final String PREFERENCES_CLI_HELP_LINK = "https://checkmarx.com/resource/documents/en/34965-68626-global-flags.html"; public static final String PREFERENCES_VALIDATING_STATE = "Validating..."; - - /******************************** TOPICS ********************************/ - public static final String TOPIC_APPLY_SETTINGS = "ApplySettings"; + public static final String LOGOUT_CONFIRM_TITLE = "Confirm Logout"; + public static final String LOGOUT_CONFIRM_MESSAGE = "Are you sure you want to logout?"; + public static final String LOGOUT_SUCCESS_MESSAGE = "You have been successfully logged out."; + public static final String AUTH_SUCCESS_DISPLAY = "You are connected to Checkmarx One"; + // Shown to the user for any authentication failure, regardless of cause - the actual + // reason is logged (see PreferencesPage.mapAuthResult()), not surfaced in the UI. + public static final String AUTH_FAILURE_DISPLAY = "Failed to connect to Checkmarx One. Please check your credentials and try again."; + public static final String TOPIC_APPLY_SETTINGS = SettingsTopics.TOPIC_APPLY_SETTINGS; /******************************** PROBLEMS VIEW ********************************/ public static final String PROBLEM_SOURCE_ID = "CheckmarxEclipsePlugin"; @@ -136,4 +151,37 @@ public class PluginConstants { public static final String CX_PROJECT_MISMATCH = "Project mismatch"; public static final String CX_PROJECT_MISMATCH_QUESTION = "The files open in your workspace don't match the files previously scanned in this Checkmarx project. Do you want to scan anyway?"; public static final String CX_REFRESHING_TOOLBAR = "Checkmarx: Refreshing toolbar..."; + + /**********************************Checkmarx One Assist************************************/ + public static final String GO_TO_CHECKMARX_ONE = "Go to Checkmarx One"; + public static final String LOGIN_NOTE_CXONE_ASSIST = "To configure Checkmarx One Assist settings, log in to Checkmarx One."; + 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 static final String[] CONTAINERS_TOOLS = new String[] { "docker", "podman" }; + + /**********************************Checkmarx MCP************************************/ + public static final String CHECKMARX_MCP_SECTION_TITLE = "Checkmarx : MCP"; + public static final String MCP_DESCRIPTION = "The Model Context Protocol (MCP) provides advanced contextual analysis for secure coding."; + public static final String INSTALL_MCP_LINK_TEXT = "Install MCP"; + public static final String EDIT_MCP_SETTINGS_LINK_TEXT = "Edit MCP Settings"; + public static final String MCP_INSTALL_UNAVAILABLE_MESSAGE = "MCP install is not available right now. Please try again after the plugin has fully started."; + public static final String MCP_INSTALLING_STATE = "Installing..."; + public static final String MCP_INSTALL_SUCCESS_MESSAGE = "Checkmarx MCP installed successfully"; + public static final String MCP_ALREADY_UP_TO_DATE_MESSAGE = "MCP configuration is already up to date."; + public static final String MCP_INSTALL_GENERIC_FAILURE_MESSAGE = "Failed to install Checkmarx MCP. Please try again."; + public static final String MCP_NOT_AUTHENTICATED_MESSAGE = "You must be connected to Checkmarx One before installing MCP."; + public static final String MCP_NOT_ENABLED_FOR_TENANT_MESSAGE = "MCP is not enabled for your Checkmarx One tenant."; + // GitHub Copilot for Eclipse's own MCP preference page - opened by "Edit MCP Settings". + public static final String COPILOT_MCP_PREFERENCE_PAGE_ID = "com.microsoft.copilot.eclipse.ui.preferences.McpPreferencePage"; } \ No newline at end of file diff --git a/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java b/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java new file mode 100644 index 00000000..461094ef --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java @@ -0,0 +1,89 @@ +package com.checkmarx.eclipse.common.wrapper; + +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.common.utils.PluginConstants; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +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 build(Preferences.getApiKey(), Preferences.getAdditionalOptions()); + } + + /** + * Create a CxWrapper with the given credentials and the current agent configuration. + * Used when the credentials being validated aren't necessarily the ones already saved + * (e.g. the Preferences page "Test Connection" action). + * + * @param apiKey the API key to authenticate with + * @param additionalParameters additional CLI parameters + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + public static CxWrapper build(String apiKey, String additionalParameters) throws CxException, Exception { + return getWrapper(apiKey, additionalParameters); + } + + /** + * Create a CxWrapper with the given credentials and configuration + * + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + private static CxWrapper getWrapper(String apiKey, String additionalParameters) throws Exception { + CxWrapper cxWrapper = null; + + Logger log = LoggerFactory.getLogger(CxWrapperFactory.class.getName()); + + CxConfig config = CxConfig.builder() + .apiKey(apiKey) + .additionalParameters(additionalParameters) + .agentName(getAgentInfo()) + .build(); + try { + cxWrapper = new CxWrapper(config, log); + } catch (IOException e) { + CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); + throw new Exception(e); + } + + return cxWrapper; + } + + /** + * Get the agent information string for the CxWrapper + * @return + */ + private static String getAgentInfo() { + String pluginVersion = getPluginVersion(); + CxLogger.info(String.format("PLUGIN_VERSION: %s_%s", PluginConstants.AGENT_NAME, pluginVersion)); + return String.format("%s_%s", PluginConstants.AGENT_NAME, pluginVersion); + } + + /** + * Resolve the version of the bundle this class ships in, as stamped by the + * build (Tycho replaces the "qualifier" placeholder in MANIFEST.MF with the + * real build qualifier), falling back when running outside an OSGi framework. + */ + private static String getPluginVersion() { + try { + Bundle bundle = FrameworkUtil.getBundle(CxWrapperFactory.class); + return bundle != null ? bundle.getVersion().toString() : "0.0.0"; + } catch (Exception e) { + CxLogger.error(String.format("Exception occurred while getting plugin version. Root cause: %s", e.getMessage()), e); + return "0.0.0"; + } + } +} \ No newline at end of file diff --git a/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java b/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java new file mode 100644 index 00000000..9a857785 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java @@ -0,0 +1,268 @@ +package com.checkmarx.eclipse.common.wrapper; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.codebashing.CodeBashing; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.learnMore.LearnMore; +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.ast.predicate.CustomState; +import com.checkmarx.ast.predicate.Predicate; +import com.checkmarx.ast.project.Project; +import com.checkmarx.ast.results.Results; +import com.checkmarx.ast.results.result.Node; +import com.checkmarx.ast.scan.Scan; +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; + +/** + * Exposes CxWrapper operations to the rest of the plugin. Every call goes + * through CxWrapperFactory so the wrapper is always built with the current + * credentials and agent information. + */ +public class WrapperProvider { + + /** + * Authenticate with the given credentials, independently of what is currently saved + * in Preferences (e.g. the Preferences page "Test Connection" action). + * @param apiKey + * @param additionalParameters + * @return + * @throws Exception + */ + public String authValidate(String apiKey, String additionalParameters) throws Exception { + return CxWrapperFactory.build(apiKey, additionalParameters).authValidate(); + } + + /** + * Gets the list of projects from the Checkmarx API, optionally filter the results. + * @param filter + * @return + * @throws Exception + */ + public List getProjects(String filter) throws Exception { + return CxWrapperFactory.build().projectList(filter); + } + + /** + * Fetch a single project directly by its ID. + * @param projectId + * @return + * @throws Exception + */ + public Project projectShow(UUID projectId) throws Exception { + return CxWrapperFactory.build().projectShow(projectId); + } + + /** + * Get branches for a specific project. + * @param projectId + * @param filter + * @return + * @throws Exception + */ + public List projectBranches(UUID projectId, String filter) throws Exception { + return CxWrapperFactory.build().projectBranches(projectId, filter); + } + + /** + * Get scans matching the given filter. + * @param filter + * @return + * @throws Exception + */ + public List scanList(String filter) throws Exception { + return CxWrapperFactory.build().scanList(filter); + } + + /** + * Get scan information for a specific scan id. + * @param scanId + * @return + * @throws Exception + */ + public Scan scanShow(UUID scanId) throws Exception { + return CxWrapperFactory.build().scanShow(scanId); + } + + /** + * Create a scan for the given source path/project/branch. + * @param scanArguments + * @param additionalParameters + * @return + * @throws Exception + */ + public Scan scanCreate(Map scanArguments, String additionalParameters) throws Exception { + return CxWrapperFactory.build().scanCreate(scanArguments, additionalParameters); + } + + /** + * Cancel a running scan. + * @param scanId + * @throws Exception + */ + public void scanCancel(String scanId) throws Exception { + CxWrapperFactory.build().scanCancel(scanId); + } + + /** + * Get results for a specific scan id. + * @param scanId + * @param agent + * @return + * @throws Exception + */ + public Results results(UUID scanId, String agent) throws Exception { + return CxWrapperFactory.build().results(scanId, agent); + } + + /** + * Get the codeBashing lessons matching a CWE/language/query name. + * @param cwe + * @param language + * @param queryName + * @return + * @throws Exception + */ + public List codeBashingList(String cwe, String language, String queryName) throws Exception { + return CxWrapperFactory.build().codeBashingList(cwe, language, queryName); + } + + /** + * Get the best fix location among the given nodes. + * @param scanId + * @param queryId + * @param bflNodes + * @return + * @throws Exception + */ + public int getResultsBfl(UUID scanId, String queryId, List bflNodes) throws Exception { + return CxWrapperFactory.build().getResultsBfl(scanId, queryId, bflNodes); + } + + /** + * Get triage details for a similarity id. + * @param projectId + * @param similarityId + * @param scanType + * @return + * @throws Exception + */ + public List triageShow(UUID projectId, String similarityId, String scanType) throws Exception { + return CxWrapperFactory.build().triageShow(projectId, similarityId, scanType); + } + + /** + * Update a vulnerability severity or state. + * @param projectId + * @param similarityId + * @param engineType + * @param state + * @param comment + * @param severity + * @throws Exception + */ + public void triageUpdate(UUID projectId, String similarityId, String engineType, String state, String comment, + String severity) throws Exception { + CxWrapperFactory.build().triageUpdate(projectId, similarityId, engineType, state, comment, severity); + } + + /** + * Triages the states from the Checkmarx API, optionally forcing a refresh of the cached states. + * @param forceRefresh + * @return + * @throws Exception + */ + public List triageGetStates(boolean forceRefresh) throws Exception { + return CxWrapperFactory.build().triageGetStates(forceRefresh); + } + + /** + * Get learn more information for a query. + * @param queryId + * @return + * @throws Exception + */ + public List learnMore(String queryId) throws Exception { + return CxWrapperFactory.build().learnMore(queryId); + } + + /** + * Check if scanning from the IDE is allowed for the current tenant. + * @return + * @throws Exception + */ + public boolean ideScansEnabled() throws Exception { + return CxWrapperFactory.build().ideScansEnabled(); + } + + /** + * Check if AI MCP (Checkmarx One Assist) is enabled for the current tenant. + * @return + * @throws Exception + */ + public boolean isAiMcpServerEnabled(String apiKey, String additionalParameter) throws Exception { + return CxWrapperFactory.build(apiKey, additionalParameter).aiMcpServerEnabled(); + } + + /** + * Run a Checkmarx ASCA (AI Security Code Assistant) realtime scan on a file. + * @param path + * @param latestVersion + * @param agent + * @param ignoreFilePath + * @return + * @throws Exception + */ + public ScanResult scanAsca(String path, boolean latestVersion, String agent, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().ScanAsca(path, latestVersion, agent, ignoreFilePath); + } + + /** + * Run a Checkmarx OSS (Software Composition Analysis) realtime scan on a manifest file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public OssRealtimeResults ossRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().ossRealtimeScan(path, ignoreFilePath); + } + + /** + * Run a Checkmarx Containers realtime scan on a file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public ContainersRealtimeResults containersRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().containersRealtimeScan(path, ignoreFilePath); + } + + /** + * Run a Checkmarx IaC realtime scan on a file. + * @param path + * @param containerTool + * @param ignoreFilePath + * @return + * @throws Exception + */ + public IacRealtimeResults iacRealtimeScan(String path, String containerTool, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().iacRealtimeScan(path, containerTool, ignoreFilePath); + } + + /** + * Run a Checkmarx Secrets realtime scan on a file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public SecretsRealtimeResults secretsRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().secretsRealtimeScan(path, ignoreFilePath); + } +} diff --git a/devassist-lib/.classpath b/devassist-lib/.classpath new file mode 100644 index 00000000..cca0ae6a --- /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..326e02f3 --- /dev/null +++ b/devassist-lib/META-INF/MANIFEST.MF @@ -0,0 +1,36 @@ +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.ui.genericeditor, + 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, + org.eclipse.jdt.ui +Import-Package: com.fasterxml.jackson.annotation, + com.fasterxml.jackson.core, + com.fasterxml.jackson.core.type, + com.fasterxml.jackson.databind, + 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, + com.checkmarx.eclipse.devassist.ignore, + com.checkmarx.eclipse.devassist.ui.findings.ignore 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/critical_20.svg b/devassist-lib/icons/critical_20.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/devassist-lib/icons/critical_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/critical_20_dark.svg b/devassist-lib/icons/critical_20_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/devassist-lib/icons/critical_20_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/high_20.svg b/devassist-lib/icons/high_20.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/devassist-lib/icons/high_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/high_20_dark.svg b/devassist-lib/icons/high_20_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/devassist-lib/icons/high_20_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/ignored_20.svg b/devassist-lib/icons/ignored_20.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/devassist-lib/icons/ignored_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_20_dark.svg b/devassist-lib/icons/ignored_20_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/devassist-lib/icons/ignored_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-critical.svg b/devassist-lib/icons/ignored_card/card-containers-critical.svg new file mode 100644 index 00000000..a8e19cae --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-critical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-critical_dark.svg b/devassist-lib/icons/ignored_card/card-containers-critical_dark.svg new file mode 100644 index 00000000..06d6848b --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-critical_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-high.svg b/devassist-lib/icons/ignored_card/card-containers-high.svg new file mode 100644 index 00000000..8aed7382 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-high.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-high_dark.svg b/devassist-lib/icons/ignored_card/card-containers-high_dark.svg new file mode 100644 index 00000000..b586724e --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-high_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-low.svg b/devassist-lib/icons/ignored_card/card-containers-low.svg new file mode 100644 index 00000000..9051c831 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-low.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-low_dark.svg b/devassist-lib/icons/ignored_card/card-containers-low_dark.svg new file mode 100644 index 00000000..96efafe1 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-low_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-malicious.svg b/devassist-lib/icons/ignored_card/card-containers-malicious.svg new file mode 100644 index 00000000..6dd33af0 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-malicious.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-malicious_dark.svg b/devassist-lib/icons/ignored_card/card-containers-malicious_dark.svg new file mode 100644 index 00000000..dd4a0b2a --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-malicious_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-medium.svg b/devassist-lib/icons/ignored_card/card-containers-medium.svg new file mode 100644 index 00000000..e4e5c61b --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-medium.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-containers-medium_dark.svg b/devassist-lib/icons/ignored_card/card-containers-medium_dark.svg new file mode 100644 index 00000000..84a3ecfb --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-containers-medium_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-critical.svg b/devassist-lib/icons/ignored_card/card-package-critical.svg new file mode 100644 index 00000000..7b80b169 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-critical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-critical_dark.svg b/devassist-lib/icons/ignored_card/card-package-critical_dark.svg new file mode 100644 index 00000000..4f622d39 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-critical_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-high.svg b/devassist-lib/icons/ignored_card/card-package-high.svg new file mode 100644 index 00000000..dcb0319d --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-high.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-high_dark.svg b/devassist-lib/icons/ignored_card/card-package-high_dark.svg new file mode 100644 index 00000000..c09f9ed5 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-high_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-low.svg b/devassist-lib/icons/ignored_card/card-package-low.svg new file mode 100644 index 00000000..8615dd0b --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-low.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-low_dark.svg b/devassist-lib/icons/ignored_card/card-package-low_dark.svg new file mode 100644 index 00000000..f6bc934c --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-low_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-malicious.svg b/devassist-lib/icons/ignored_card/card-package-malicious.svg new file mode 100644 index 00000000..38856995 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-malicious.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-malicious_dark.svg b/devassist-lib/icons/ignored_card/card-package-malicious_dark.svg new file mode 100644 index 00000000..4bdb763b --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-malicious_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-medium.svg b/devassist-lib/icons/ignored_card/card-package-medium.svg new file mode 100644 index 00000000..4c2ee230 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-medium.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-package-medium_dark.svg b/devassist-lib/icons/ignored_card/card-package-medium_dark.svg new file mode 100644 index 00000000..1ae43a36 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-package-medium_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-critical.svg b/devassist-lib/icons/ignored_card/card-secret-critical.svg new file mode 100644 index 00000000..600777a2 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-critical.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-critical_dark.svg b/devassist-lib/icons/ignored_card/card-secret-critical_dark.svg new file mode 100644 index 00000000..eceb3949 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-critical_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-high.svg b/devassist-lib/icons/ignored_card/card-secret-high.svg new file mode 100644 index 00000000..d8b99816 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-high.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-high_dark.svg b/devassist-lib/icons/ignored_card/card-secret-high_dark.svg new file mode 100644 index 00000000..2cd599a1 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-high_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-low.svg b/devassist-lib/icons/ignored_card/card-secret-low.svg new file mode 100644 index 00000000..68753134 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-low.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-low_dark.svg b/devassist-lib/icons/ignored_card/card-secret-low_dark.svg new file mode 100644 index 00000000..1ade2bba --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-low_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-malicious.svg b/devassist-lib/icons/ignored_card/card-secret-malicious.svg new file mode 100644 index 00000000..a3e0ad6f --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-malicious.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-malicious_dark.svg b/devassist-lib/icons/ignored_card/card-secret-malicious_dark.svg new file mode 100644 index 00000000..247688bf --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-malicious_dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-medium.svg b/devassist-lib/icons/ignored_card/card-secret-medium.svg new file mode 100644 index 00000000..295ef9a1 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-medium.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-secret-medium_dark.svg b/devassist-lib/icons/ignored_card/card-secret-medium_dark.svg new file mode 100644 index 00000000..f1b7d6a1 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-secret-medium_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-critical.svg b/devassist-lib/icons/ignored_card/card-vulnerability-critical.svg new file mode 100644 index 00000000..c6d8da79 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-critical.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-critical_dark.svg b/devassist-lib/icons/ignored_card/card-vulnerability-critical_dark.svg new file mode 100644 index 00000000..557ee013 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-critical_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-high.svg b/devassist-lib/icons/ignored_card/card-vulnerability-high.svg new file mode 100644 index 00000000..02fe3b64 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-high.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-high_dark.svg b/devassist-lib/icons/ignored_card/card-vulnerability-high_dark.svg new file mode 100644 index 00000000..8c82a8bc --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-high_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-low.svg b/devassist-lib/icons/ignored_card/card-vulnerability-low.svg new file mode 100644 index 00000000..dbf34b5e --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-low.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-low_dark.svg b/devassist-lib/icons/ignored_card/card-vulnerability-low_dark.svg new file mode 100644 index 00000000..08f2243a --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-low_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-malicious.svg b/devassist-lib/icons/ignored_card/card-vulnerability-malicious.svg new file mode 100644 index 00000000..f22a83c1 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-malicious.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-malicious_dark.svg b/devassist-lib/icons/ignored_card/card-vulnerability-malicious_dark.svg new file mode 100644 index 00000000..3f1c080d --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-malicious_dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-medium.svg b/devassist-lib/icons/ignored_card/card-vulnerability-medium.svg new file mode 100644 index 00000000..e6c7461b --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-medium.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devassist-lib/icons/ignored_card/card-vulnerability-medium_dark.svg b/devassist-lib/icons/ignored_card/card-vulnerability-medium_dark.svg new file mode 100644 index 00000000..87822132 --- /dev/null +++ b/devassist-lib/icons/ignored_card/card-vulnerability-medium_dark.svg @@ -0,0 +1,6 @@ + + + + + + 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/low_20.svg b/devassist-lib/icons/low_20.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/devassist-lib/icons/low_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/low_20_dark.svg b/devassist-lib/icons/low_20_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/devassist-lib/icons/low_20_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/malicious_20.svg b/devassist-lib/icons/malicious_20.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/devassist-lib/icons/malicious_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/malicious_20_dark.svg b/devassist-lib/icons/malicious_20_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/devassist-lib/icons/malicious_20_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/medium_20.svg b/devassist-lib/icons/medium_20.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/devassist-lib/icons/medium_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/medium_20_dark.svg b/devassist-lib/icons/medium_20_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/devassist-lib/icons/medium_20_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/severity_16/critical.svg b/devassist-lib/icons/severity_16/critical.svg new file mode 100644 index 00000000..6e1929e8 --- /dev/null +++ b/devassist-lib/icons/severity_16/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/critical_dark.svg b/devassist-lib/icons/severity_16/critical_dark.svg new file mode 100644 index 00000000..9c89888d --- /dev/null +++ b/devassist-lib/icons/severity_16/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/high.svg b/devassist-lib/icons/severity_16/high.svg new file mode 100644 index 00000000..4c815e84 --- /dev/null +++ b/devassist-lib/icons/severity_16/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/high_dark.svg b/devassist-lib/icons/severity_16/high_dark.svg new file mode 100644 index 00000000..d9b8a81f --- /dev/null +++ b/devassist-lib/icons/severity_16/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ignored.svg b/devassist-lib/icons/severity_16/ignored.svg new file mode 100644 index 00000000..4ec04da0 --- /dev/null +++ b/devassist-lib/icons/severity_16/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ignored_dark.svg b/devassist-lib/icons/severity_16/ignored_dark.svg new file mode 100644 index 00000000..20246d56 --- /dev/null +++ b/devassist-lib/icons/severity_16/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/low.svg b/devassist-lib/icons/severity_16/low.svg new file mode 100644 index 00000000..40b203e4 --- /dev/null +++ b/devassist-lib/icons/severity_16/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/low_dark.svg b/devassist-lib/icons/severity_16/low_dark.svg new file mode 100644 index 00000000..69f9b3a6 --- /dev/null +++ b/devassist-lib/icons/severity_16/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/malicious.svg b/devassist-lib/icons/severity_16/malicious.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/severity_16/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/malicious_dark.svg b/devassist-lib/icons/severity_16/malicious_dark.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/severity_16/malicious_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/medium.svg b/devassist-lib/icons/severity_16/medium.svg new file mode 100644 index 00000000..3a6cda49 --- /dev/null +++ b/devassist-lib/icons/severity_16/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/medium_dark.svg b/devassist-lib/icons/severity_16/medium_dark.svg new file mode 100644 index 00000000..5be2c823 --- /dev/null +++ b/devassist-lib/icons/severity_16/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ok.svg b/devassist-lib/icons/severity_16/ok.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/severity_16/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ok_dark.svg b/devassist-lib/icons/severity_16/ok_dark.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/severity_16/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/unknown.svg b/devassist-lib/icons/severity_16/unknown.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/devassist-lib/icons/severity_16/unknown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_16/unknown_dark.svg b/devassist-lib/icons/severity_16/unknown_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/devassist-lib/icons/severity_16/unknown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_20/critical.svg b/devassist-lib/icons/severity_20/critical.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/devassist-lib/icons/severity_20/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/critical_dark.svg b/devassist-lib/icons/severity_20/critical_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/devassist-lib/icons/severity_20/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/high.svg b/devassist-lib/icons/severity_20/high.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/devassist-lib/icons/severity_20/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/high_dark.svg b/devassist-lib/icons/severity_20/high_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/devassist-lib/icons/severity_20/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ignored.svg b/devassist-lib/icons/severity_20/ignored.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/devassist-lib/icons/severity_20/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ignored_dark.svg b/devassist-lib/icons/severity_20/ignored_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/devassist-lib/icons/severity_20/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/low.svg b/devassist-lib/icons/severity_20/low.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/devassist-lib/icons/severity_20/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/low_dark.svg b/devassist-lib/icons/severity_20/low_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/devassist-lib/icons/severity_20/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/malicious.svg b/devassist-lib/icons/severity_20/malicious.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/devassist-lib/icons/severity_20/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/malicious_dark.svg b/devassist-lib/icons/severity_20/malicious_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/devassist-lib/icons/severity_20/malicious_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/medium.svg b/devassist-lib/icons/severity_20/medium.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/devassist-lib/icons/severity_20/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/medium_dark.svg b/devassist-lib/icons/severity_20/medium_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/devassist-lib/icons/severity_20/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ok.svg b/devassist-lib/icons/severity_20/ok.svg new file mode 100644 index 00000000..dc746080 --- /dev/null +++ b/devassist-lib/icons/severity_20/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ok_dark.svg b/devassist-lib/icons/severity_20/ok_dark.svg new file mode 100644 index 00000000..c139bab4 --- /dev/null +++ b/devassist-lib/icons/severity_20/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/critical.svg b/devassist-lib/icons/severity_24/critical.svg new file mode 100644 index 00000000..b53aebfc --- /dev/null +++ b/devassist-lib/icons/severity_24/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/critical_dark.svg b/devassist-lib/icons/severity_24/critical_dark.svg new file mode 100644 index 00000000..162d5016 --- /dev/null +++ b/devassist-lib/icons/severity_24/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/high.svg b/devassist-lib/icons/severity_24/high.svg new file mode 100644 index 00000000..50837da7 --- /dev/null +++ b/devassist-lib/icons/severity_24/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/high_dark.svg b/devassist-lib/icons/severity_24/high_dark.svg new file mode 100644 index 00000000..01ab7f2d --- /dev/null +++ b/devassist-lib/icons/severity_24/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ignored.svg b/devassist-lib/icons/severity_24/ignored.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/devassist-lib/icons/severity_24/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ignored_dark.svg b/devassist-lib/icons/severity_24/ignored_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/devassist-lib/icons/severity_24/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/low.svg b/devassist-lib/icons/severity_24/low.svg new file mode 100644 index 00000000..a9e7b0ec --- /dev/null +++ b/devassist-lib/icons/severity_24/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/low_dark.svg b/devassist-lib/icons/severity_24/low_dark.svg new file mode 100644 index 00000000..cfdc04b9 --- /dev/null +++ b/devassist-lib/icons/severity_24/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/malicious.svg b/devassist-lib/icons/severity_24/malicious.svg new file mode 100644 index 00000000..9c78e5cf --- /dev/null +++ b/devassist-lib/icons/severity_24/malicious.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_24/malicious_dark.svg b/devassist-lib/icons/severity_24/malicious_dark.svg new file mode 100644 index 00000000..5635eced --- /dev/null +++ b/devassist-lib/icons/severity_24/malicious_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_24/medium.svg b/devassist-lib/icons/severity_24/medium.svg new file mode 100644 index 00000000..fb1458c6 --- /dev/null +++ b/devassist-lib/icons/severity_24/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/medium_dark.svg b/devassist-lib/icons/severity_24/medium_dark.svg new file mode 100644 index 00000000..0eb1ba32 --- /dev/null +++ b/devassist-lib/icons/severity_24/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ok.svg b/devassist-lib/icons/severity_24/ok.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/devassist-lib/icons/severity_24/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ok_dark.svg b/devassist-lib/icons/severity_24/ok_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/devassist-lib/icons/severity_24/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/star-action.svg b/devassist-lib/icons/star-action.svg new file mode 100644 index 00000000..bfc23248 --- /dev/null +++ b/devassist-lib/icons/star-action.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/devassist-lib/icons/tooltip/container.png b/devassist-lib/icons/tooltip/container.png new file mode 100644 index 00000000..15333f2a Binary files /dev/null and b/devassist-lib/icons/tooltip/container.png differ diff --git a/devassist-lib/icons/tooltip/container_dark.png b/devassist-lib/icons/tooltip/container_dark.png new file mode 100644 index 00000000..71980914 Binary files /dev/null and b/devassist-lib/icons/tooltip/container_dark.png differ diff --git a/devassist-lib/icons/tooltip/critical.png b/devassist-lib/icons/tooltip/critical.png new file mode 100644 index 00000000..5ebf58ac Binary files /dev/null and b/devassist-lib/icons/tooltip/critical.png differ diff --git a/devassist-lib/icons/tooltip/critical_dark.png b/devassist-lib/icons/tooltip/critical_dark.png new file mode 100644 index 00000000..98aff91b Binary files /dev/null and b/devassist-lib/icons/tooltip/critical_dark.png differ diff --git a/devassist-lib/icons/tooltip/cxone_assist.png b/devassist-lib/icons/tooltip/cxone_assist.png new file mode 100644 index 00000000..6c2c1434 Binary files /dev/null and b/devassist-lib/icons/tooltip/cxone_assist.png differ diff --git a/devassist-lib/icons/tooltip/cxone_assist_dark.png b/devassist-lib/icons/tooltip/cxone_assist_dark.png new file mode 100644 index 00000000..2962c7a9 Binary files /dev/null and b/devassist-lib/icons/tooltip/cxone_assist_dark.png differ diff --git a/devassist-lib/icons/tooltip/devassist_badge.png b/devassist-lib/icons/tooltip/devassist_badge.png new file mode 100644 index 00000000..2deaf922 Binary files /dev/null and b/devassist-lib/icons/tooltip/devassist_badge.png differ diff --git a/devassist-lib/icons/tooltip/devassist_badge_dark.png b/devassist-lib/icons/tooltip/devassist_badge_dark.png new file mode 100644 index 00000000..f8ee0be6 Binary files /dev/null and b/devassist-lib/icons/tooltip/devassist_badge_dark.png differ diff --git a/devassist-lib/icons/tooltip/high.png b/devassist-lib/icons/tooltip/high.png new file mode 100644 index 00000000..594c3ef3 Binary files /dev/null and b/devassist-lib/icons/tooltip/high.png differ diff --git a/devassist-lib/icons/tooltip/high_dark.png b/devassist-lib/icons/tooltip/high_dark.png new file mode 100644 index 00000000..8251b640 Binary files /dev/null and b/devassist-lib/icons/tooltip/high_dark.png differ diff --git a/devassist-lib/icons/tooltip/low.png b/devassist-lib/icons/tooltip/low.png new file mode 100644 index 00000000..d0f4bb3b Binary files /dev/null and b/devassist-lib/icons/tooltip/low.png differ diff --git a/devassist-lib/icons/tooltip/low_dark.png b/devassist-lib/icons/tooltip/low_dark.png new file mode 100644 index 00000000..545c1765 Binary files /dev/null and b/devassist-lib/icons/tooltip/low_dark.png differ diff --git a/devassist-lib/icons/tooltip/malicious.png b/devassist-lib/icons/tooltip/malicious.png new file mode 100644 index 00000000..6e145e36 Binary files /dev/null and b/devassist-lib/icons/tooltip/malicious.png differ diff --git a/devassist-lib/icons/tooltip/malicious_dark.png b/devassist-lib/icons/tooltip/malicious_dark.png new file mode 100644 index 00000000..6e145e36 Binary files /dev/null and b/devassist-lib/icons/tooltip/malicious_dark.png differ diff --git a/devassist-lib/icons/tooltip/medium.png b/devassist-lib/icons/tooltip/medium.png new file mode 100644 index 00000000..ecfb6fa4 Binary files /dev/null and b/devassist-lib/icons/tooltip/medium.png differ diff --git a/devassist-lib/icons/tooltip/medium_dark.png b/devassist-lib/icons/tooltip/medium_dark.png new file mode 100644 index 00000000..f5e88250 Binary files /dev/null and b/devassist-lib/icons/tooltip/medium_dark.png differ diff --git a/devassist-lib/icons/tooltip/package.png b/devassist-lib/icons/tooltip/package.png new file mode 100644 index 00000000..ce6048f0 Binary files /dev/null and b/devassist-lib/icons/tooltip/package.png differ diff --git a/devassist-lib/icons/tooltip/package_dark.png b/devassist-lib/icons/tooltip/package_dark.png new file mode 100644 index 00000000..899a77f1 Binary files /dev/null and b/devassist-lib/icons/tooltip/package_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/critical.png b/devassist-lib/icons/tooltip/severity_count/critical.png new file mode 100644 index 00000000..8b8a7e56 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/critical.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/critical_dark.png b/devassist-lib/icons/tooltip/severity_count/critical_dark.png new file mode 100644 index 00000000..c743dd71 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/critical_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/high.png b/devassist-lib/icons/tooltip/severity_count/high.png new file mode 100644 index 00000000..fc36e929 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/high.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/high_dark.png b/devassist-lib/icons/tooltip/severity_count/high_dark.png new file mode 100644 index 00000000..4ac2fc36 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/high_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/low.png b/devassist-lib/icons/tooltip/severity_count/low.png new file mode 100644 index 00000000..a0574bce Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/low.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/low_dark.png b/devassist-lib/icons/tooltip/severity_count/low_dark.png new file mode 100644 index 00000000..81df61cf Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/low_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/medium.png b/devassist-lib/icons/tooltip/severity_count/medium.png new file mode 100644 index 00000000..c0b5679f Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/medium.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/medium_dark.png b/devassist-lib/icons/tooltip/severity_count/medium_dark.png new file mode 100644 index 00000000..296a081b Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/medium_dark.png differ 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..d2c221b1 --- /dev/null +++ b/devassist-lib/plugin.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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/AuthenticationStateListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/AuthenticationStateListener.java new file mode 100644 index 00000000..0b96fbc5 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/AuthenticationStateListener.java @@ -0,0 +1,92 @@ +package com.checkmarx.eclipse.devassist.backend; + +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; + +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import java.util.EnumSet; + +/** + * Listens for authentication state changes (CREDENTIALS_VALIDATED flag) and triggers + * workspace scan when user logs in. + * + * Problem it solves: + * - When user logs in, no preferences change, so ScannerPreferencesListener doesn't trigger scan + * - But we still need to scan projects that were opened before authentication + * + * Solution: + * - Listen to CREDENTIALS_VALIDATED changes + * - When it becomes true (login), trigger workspace scan immediately + * - ScannerPreferencesListener handles preference changes separately + */ +public class AuthenticationStateListener implements IPropertyChangeListener { + + private static final String LOG_TAG = "[AUTH-STATE-LISTENER]"; + + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event == null || event.getProperty() == null) { + return; + } + + // Only respond to authentication state changes + if (!Preferences.CREDENTIALS_VALIDATED.equals(event.getProperty())) { + return; + } + + Object newValue = event.getNewValue(); + boolean nowAuthenticated = newValue instanceof Boolean && (Boolean) newValue; + + if (nowAuthenticated) { + // Handle login: clear scan cache and trigger workspace scan + handleLogin(); + } else { + // Handle logout: remove MCP server configuration + handleLogout(); + } + } + + private void handleLogin() { + CxLogger.info(LOG_TAG + " User authenticated - clearing scan cache and triggering workspace scan..."); + + // CRITICAL: Clear scan state cache so files that were never scanned (before authentication) + // are not treated as "unchanged" and skipped. Without this, files show as "cached/unchanged" + // and the scan is skipped even though they were never actually scanned before. + try { + CxLogger.info(LOG_TAG + " Clearing scan state cache for all scanners..."); + EnumSet allScanners = EnumSet.allOf(ScannerType.class); + ScanStateCacheClearer.clearForScanners(allScanners); + CxLogger.info(LOG_TAG + " ✓ Scan cache cleared"); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing scan cache: " + e.getMessage()); + } + + // Now trigger workspace scan with cleared cache + IWorkspaceScanService scanService = Preferences.getWorkspaceScanService(); + if (scanService != null) { + try { + scanService.scanWorkspace(); + CxLogger.info(LOG_TAG + " ✓ Workspace scan triggered on login"); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error triggering workspace scan: " + e.getMessage(), e); + } + } else { + CxLogger.warning(LOG_TAG + " Workspace scan service not available"); + } + } + + private void handleLogout() { + CxLogger.info(LOG_TAG + " User logged out - removing MCP server configuration..."); + + try { + // Import statement needed: com.checkmarx.eclipse.devassist.configuration.McpInstallService + com.checkmarx.eclipse.devassist.configuration.McpInstallService.uninstall(); + CxLogger.info(LOG_TAG + " ✓ MCP server configuration removed on logout"); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error removing MCP configuration on logout: " + e.getMessage(), e); + } + } +} 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..42a8d90b --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java @@ -0,0 +1,213 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +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 + // Using CopyOnWriteArrayList for thread-safe concurrent iteration and mutation + private final List stateListeners = new CopyOnWriteArrayList<>(); + + /** + * 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. + * Thread-safe: can be called concurrently with notifications. + * + * @param listener Listener callback + */ + public void addScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.add(listener); + } + } + + /** + * Unregister a state listener. + * Thread-safe: can be called concurrently with notifications. + * + * @param listener Listener to remove + */ + public void removeScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.remove(listener); + } + } + + /** + * Notify all listeners of a scanner state change. + * Thread-safe: listeners can register/unregister concurrently without + * ConcurrentModificationException. + * + * @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..17950c74 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerMarkerPurger.java @@ -0,0 +1,111 @@ +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); + // Filter to only exclude ignored issues - include OK/UNKNOWN gutter icons + com.checkmarx.eclipse.devassist.ignore.IgnoreManager ignoreManager = + com.checkmarx.eclipse.devassist.ignore.IgnoreManager.getInstance(project); + java.util.List activeIssues = + new java.util.ArrayList<>(); + for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : remaining) { + if (issue == null) { + continue; + } + if (!ignoreManager.isIgnored(issue)) { + activeIssues.add(issue); + } + } + ProblemDecorator.decorateEditor(file, activeIssues); + } + } + } 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..34206253 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -0,0 +1,428 @@ +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 volatile boolean disposed = false; + private final Object lock = new Object(); + + /** + * 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). + * Synchronized to prevent race with getScannerService() lazy creation. + */ + public void deregisterAllScanners() { + synchronized (lock) { + 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. + * + * Synchronized with deregisterAllScanners() to prevent race: + * if project closes while scanner is being created, the new instance + * will be disposed immediately and not leak. + * + * @param type Scanner type (OSS, SECRETS, etc.) + * @return Scanner instance, or null if scanner type not supported + */ + public Object getScannerService(ScannerType type) { + synchronized (lock) { + 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 { + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer, not a + // brand-new empty Document. A new Document() has no content, so + // getFileContent() falls back to reading the file from DISK - + // meaning unsaved edits (e.g. deleting a vulnerable line) would + // never be seen by the scanner until the file is saved. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : 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 { + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : 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 { + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : 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 { + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : 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 { + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : 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..81a2ce87 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java @@ -0,0 +1,498 @@ +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<>(); + + // Strong reference to the single instance registered by PluginStartup at + // earlyStartup(). Lets other devassist-lib code (e.g. CxFindingsView's + // "navigate to finding" flow, which opens/activates an editor directly + // without going through the workbench's IPartListener2 events) reuse this + // listener's per-document dedup bookkeeping instead of registering a second, + // independent CheckmarxDocumentListener/RealTimeScanJob pair on the same + // document - duplicate pairs each independently debounce and reschedule + // scans for the same file, colliding on the shared per-file in-flight lock + // in DevAssistScanStateHolder and causing scans to appear to trigger late + // (an edit's scan gets silently BLOCKED by an unrelated duplicate job that + // is still in-flight, with nothing rescheduling it afterwards). + private static volatile CheckmarxEditorListener instance; + + public CheckmarxEditorListener() { + instance = this; + } + + /** + * @return the single CheckmarxEditorListener registered at plugin startup, + * or null if it has not been created yet. + */ + public static CheckmarxEditorListener getInstance() { + return instance; + } + + /** + * Ensure real-time scanning is set up for the given editor, reusing an + * existing document listener/scan job if one is already registered for its + * document instead of creating a duplicate. + * + * This is the same dedup logic as {@link #partActivated}, exposed for + * callers that open/activate an editor programmatically (bypassing + * IPartListener2 events), such as CxFindingsView's navigate-to-finding flow. + * + * @param editor the editor part to ensure scanning is set up for + */ + public void ensureRealtimeScanningForEditor(IEditorPart editor) { + if (editor == null) { + return; + } + IDocument document = getDocumentFromEditor(editor); + if (document != null) { + int documentId = document.hashCode(); + if (activeListeners.containsKey(documentId)) { + RealTimeScanJob scanJob = activeScanJobs.get(documentId); + if (scanJob != null) { + scanJob.reschedule(0); + } + return; + } + } + setupRealtimeScanning(editor); + } + + /** + * 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) { + ensureRealtimeScanningForEditor((IEditorPart) part); + } + } 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 in editor listener's tracking + RealTimeScanJob scanJob = activeScanJobs.remove(documentId); + if (scanJob != null) { + scanJob.cancel(); + } + + // Also cancel the scheduler's pending scan for this file. + // Critical: if editor closes within debounce window (1s), scheduler's job + // still fires and publishes results for a closed editor. + org.eclipse.core.resources.IFile file = extractFileFromEditor(editor); + if (file != null) { + try { + org.eclipse.core.resources.IProject project = file.getProject(); + if (project != null) { + com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project + .getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", + "scan-scheduler")); + if (scheduler != null) { + scheduler.cancelScheduledInspection(file); + } + } + } catch (Exception e) { + System.err.println("[REALTIME] Error cancelling scheduler's pending scan: " + e.getMessage()); + } + } + } + + /** + * 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; + } + + // Filter to only exclude ignored issues - include OK/UNKNOWN gutter icons + com.checkmarx.eclipse.devassist.ignore.IgnoreManager ignoreManager = + com.checkmarx.eclipse.devassist.ignore.IgnoreManager.getInstance(project); + java.util.List activeIssues = + new java.util.ArrayList<>(); + for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : cachedIssues) { + if (issue == null) { + continue; + } + if (!ignoreManager.isIgnored(issue)) { + activeIssues.add(issue); + } + } + + // Apply decorations for non-ignored findings + ProblemDecorator.decorateEditor(file, activeIssues); + + } 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..7333693b --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -0,0 +1,462 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +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.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IContainer; +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.devassist.utils.DevAssistConstants; +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"); + + // Exclusion patterns (files/directories to skip during recursive scanning) + private static final String NODE_MODULES_EXCLUSION = "/node_modules/"; + + private final Set initializedProjects = ConcurrentHashMap.newKeySet(); + + /** + * 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; + // A project's open/closed state toggling on an EXISTING project sets the + // OPEN flag. A brand-new project being imported/created (e.g. into a + // workspace that had no projects open yet) is reported as ADDED instead - + // it never had a "closed" state to transition from, so the OPEN flag is + // never set on its delta. Checking only the OPEN flag (as before) silently + // skipped this case, leaving a newly opened project's files never scanned + // until the user manually closed/reopened it or restarted Eclipse. + boolean openStateChanged = (delta.getFlags() & IResourceDelta.OPEN) != 0; + boolean isNewProject = (delta.getKind() & IResourceDelta.ADDED) != 0; + if (openStateChanged || isNewProject) { + 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 projectName = project.getName(); + if (projectName.length() > 26) + projectName = projectName.substring(0, 26); + try { + if (!isUserAuthenticated()) { + return; + } + // Atomically mark as initialized: if add() returns false, another thread beat + // us to it. + // This prevents duplicate ScannerRegistry, ProblemHolderService, and + // workspace-scan jobs. + if (!initializedProjects.add(projectName)) { + 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); + + startWorkspaceFileScanning(project); + + } catch (Exception e) { + // Remove from initialized set on error so it can be retried + initializedProjects.remove(projectName); + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error initializing project " + + projectName + ": " + e.getMessage(), e); + } + } + + private boolean isUserAuthenticated() { + return com.checkmarx.eclipse.common.preferences.Preferences.isAuthenticated(); + } + + 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()); + } + + try { + com.checkmarx.eclipse.devassist.ignore.IgnoreManager.dispose(project); + CxLogger.info(LOG_TAG + " ✓ IgnoreManager disposed"); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing IgnoreManager: " + e.getMessage()); + } + + try { + // Unregisters the project's workspace-level ignore-file resource-change + // listener - unlike ScannerRegistry/ProblemHolderService/DevAssistScanStateHolder + // above (held in IProject session properties, auto-discarded by Eclipse on + // close), IgnoreFileManager is cached in its own static map with no lifecycle + // hook, so without this call it and its listener live for the process lifetime. + com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager.dispose(project); + CxLogger.info(LOG_TAG + " ✓ IgnoreFileManager disposed"); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing IgnoreFileManager: " + 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 OSS manifest files...", 1); + + // Check if job was cancelled or project closed before starting + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + // Only scan OSS manifests on startup (matches JetBrains behavior) + // IaC and Container scanning are triggered by real-time scanner events + scanManifestFiles(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) { + findAndScanFilesRecursive(project, DevAssistConstants.MANIFEST_FILE_PATTERNS, "OSS Manifest Files"); + } + + private void scanIacFiles(IProject project) { + findAndScanFilesRecursive(project, DevAssistConstants.IAC_SUPPORTED_PATTERNS, "IaC Configuration Files"); + } + + private void scanContainerFiles(IProject project) { + findAndScanFilesRecursive(project, DevAssistConstants.CONTAINERS_FILE_PATTERNS, "Container Files"); + } + + /** + * Recursively scans files in project matching glob patterns. + * Unlike the old root-only scan, this finds files at ANY directory depth. + * Excludes node_modules directories (and their contents) for performance. + */ + private void findAndScanFilesRecursive(IProject project, List patterns, String fileType) { + try { + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + + if (registry == null || stateHolder == null || problemHolder == null) { + CxLogger.warning(LOG_TAG + " Cannot scan " + fileType + " - registry/stateHolder/problemHolder is null"); + return; + } + + // Convert glob patterns to PathMatchers + List matchers = patterns.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Starting recursive scan for " + fileType + " with " + patterns.size() + " patterns"); + + // Recursively traverse all project resources + traverseResourcesRecursively(project, matchers, registry, stateHolder, problemHolder, fileType); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error scanning " + fileType, e); + } + } + + /** + * Recursively traverses project resources and scans matching files. + * Skips node_modules directories for performance. + */ + private void traverseResourcesRecursively(IResource resource, + List matchers, + ScannerRegistry registry, + DevAssistScanStateHolder stateHolder, + ProblemHolderService problemHolder, + String fileType) { + try { + if (!(resource instanceof IContainer)) { + return; + } + + IContainer container = (IContainer) resource; + IResource[] children = container.members(false); + + for (IResource child : children) { + // Safety check: resource might be deleted or not exist + if (!child.exists()) { + continue; + } + + String path = child.getLocation().toOSString(); + + // Skip node_modules (like JetBrains) - avoid scanning npm packages + if (path.contains(NODE_MODULES_EXCLUSION) || path.contains("\\node_modules\\")) { + continue; + } + + if (child instanceof IFile) { + IFile file = (IFile) child; + + // Check if file matches any of the glob patterns + for (PathMatcher matcher : matchers) { + try { + if (matcher.matches(Paths.get(path))) { + scanFileAndPublishResults(file, path, registry, stateHolder, problemHolder); + break; // File matched, don't check other patterns + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error matching pattern for " + path + ": " + e.getMessage()); + } + } + } else if (child instanceof IContainer) { + // Recurse into subdirectories + traverseResourcesRecursively(child, matchers, registry, stateHolder, problemHolder, fileType); + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error traversing resources: " + e.getMessage()); + } + } + + /** + * Scans a single file and publishes results if issues are found. + */ + private void scanFileAndPublishResults(IFile file, + String filePath, + ScannerRegistry registry, + DevAssistScanStateHolder stateHolder, + ProblemHolderService problemHolder) { + 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) { + CxLogger.warning(LOG_TAG + " Error scanning file " + filePath + ": " + e.getMessage()); + } + } + + /** + * Legacy method for backward compatibility. + * Kept as fallback but no longer used by new code. + */ + private void findAndScanFiles(IProject project, String[] patterns, String fileType) { + // Convert string array to list and use new recursive method + List patternList = new ArrayList<>(); + for (String pattern : patterns) { + patternList.add("**/*" + pattern); // Add /** prefix for recursive matching + } + findAndScanFilesRecursive(project, patternList, fileType); + } +} 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..e46915f3 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java @@ -0,0 +1,253 @@ +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"); + org.eclipse.core.runtime.QualifiedName problemHolderKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "problem-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); + + com.checkmarx.eclipse.devassist.problems.ProblemHolderService problemHolder = (com.checkmarx.eclipse.devassist.problems.ProblemHolderService) project + .getSessionProperty(problemHolderKey); + + // 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); + + } + + if (problemHolder == null) { + + problemHolder = new com.checkmarx.eclipse.devassist.problems.ProblemHolderService(); + project.setSessionProperty(problemHolderKey, problemHolder); + + } + + // Force the ignore file/temp list to reconcile with disk before scanning, + // rather than relying solely on the workspace resource-change watcher + // having already fired for the latest .checkmarxIgnored edit. Without + // this, a manual/external edit to .checkmarxIgnored that the watcher + // missed (or hasn't processed yet) would leave the CLI-facing + // .checkmarxIgnoredTempList.json stale for this scan cycle. + com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager.getInstance(project).refreshFromDisk(); + + // Execute backend scanners + com.checkmarx.eclipse.devassist.common.ScanManager scanManager = new com.checkmarx.eclipse.devassist.common.ScanManager( + registry, stateHolder); + + String filePath = file.getLocation().toOSString(); + + // Pass progress monitor into scanFile to support cancellation during scan + // execution + com.checkmarx.eclipse.devassist.common.ScanManager.ScanOutcome outcome = scanManager + .scanFileWithOutcome(filePath, monitor); + + // Re-check cancellation status right before updating UI/markers to avoid + // publishing stale results + if (monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // ✅ CRITICAL FIX: Publish results whenever a REAL scan ran, even if the + // result is empty. When a vulnerable line is DELETED, the scan returns + // 0 issues - if we skip publishResults() here, the old findings never + // get cleared from ProblemHolderService, so CxFindingsView and editor + // annotations are never updated (stale data). + // + // But we must NOT publish when the cycle was merely SKIPPED (file + // unchanged since last scan - e.g. a hover-triggered editor + // reactivation rescheduling this job with nothing actually different). + // Publishing an empty list in that case would incorrectly wipe out + // valid, still-current results/annotations for this file. + if (outcome.isScanned()) { + com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, + outcome.getIssues()); + } + + } 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 to Checkmarx One. + */ + private boolean isUserAuthenticated() { + return com.checkmarx.eclipse.common.preferences.Preferences.isAuthenticated(); + } + + @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..f3e19940 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -0,0 +1,301 @@ +package com.checkmarx.eclipse.devassist.backend.result; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.text.IDocument; +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.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. + * + * ✅ CRITICAL: `scanIssues` here always represents the COMPLETE, current set + * of issues for this file across every applicable/enabled engine - because + * {@link com.checkmarx.eclipse.devassist.common.ScanManager#scanFileWithOutcome} + * runs every applicable scanner for the file in a single pass and this method + * is only invoked by callers that just performed (or confirmed) such a real + * scan cycle (see {@link RealTimeScanJob}, which gates this call on + * {@code ScanOutcome.isScanned()}). + * + * Because of that, this is a full REPLACE of the file's cached issues, not a + * per-engine merge/remove. This correctly handles the case where a vulnerable + * line is deleted (scanIssues becomes empty -> cache is fully cleared for + * this file) without needing to infer which engine produced which result. + * + * @param file File that was scanned + * @param scanIssues Complete, current issue list for this file (may be empty) + */ + private static void updateFindingsView(IFile file, List scanIssues) { + try { + // Must run on UI thread + org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + + // 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) { + // Full replace: this cycle's scanIssues list IS the complete truth for + // this file. If it's empty, every previously-cached issue for this + // file (from any engine) is correctly dropped. This also publishes + // the ISSUES_UPDATED_TOPIC event that CxFindingsView listens to. + problemHolder.addScanIssues(filePath, scanIssues); + CxLogger.info(LOG_TAG + " Updated cache for " + filePath + " with " + scanIssues.size() + + " issues"); + + } else { + CxLogger.warning(LOG_TAG + " ProblemHolderService not initialized for project"); + } + } + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error updating findings view: " + e.getMessage(), 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 + * + * ✅ CRITICAL: Always processes results, even if empty. + * When scan returns 0 issues, we MUST clear old decorations/annotations. + * + * @param file File that was scanned + * @param scanIssues Issues to process (may be empty) + */ + private static void createAndRenderDecorations(IFile file, List scanIssues) { + try { + 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 ITextEditor) { + ITextEditor textEditor = (ITextEditor) editor; + 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..bb55fcd0 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java @@ -0,0 +1,116 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.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..4c5f8534 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -0,0 +1,264 @@ +package com.checkmarx.eclipse.devassist.common; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; + +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; + } + + /** + * Result of a scan attempt. + * + * Distinguishes a REAL scan cycle (scanners actually executed, state hash + * updated) from a SKIPPED cycle (file unchanged since last scan, or + * cancelled, or every scanner failed). Both cases can return an empty issue + * list, but only a real scan cycle means "we now know this file has these + * (possibly zero) issues" - callers must NOT treat a skipped cycle's empty + * list as "file is now clean", or they will wipe out valid cached results + * every time a no-op scan fires (e.g. editor re-activation, hover-triggered + * focus events). + */ + public static class ScanOutcome { + private final List issues; + private final boolean scanned; + + public ScanOutcome(List issues, boolean scanned) { + this.issues = issues; + this.scanned = scanned; + } + + public List getIssues() { + return issues; + } + + /** + * @return true if scanners actually ran this cycle and {@link #getIssues()} + * reflects the file's current, complete state across all applicable + * engines (even if empty). false if the cycle was skipped (file + * unchanged, cancelled, or all scanners failed) and the caller should + * leave existing cached results/decorations untouched. + */ + public boolean isScanned() { + return scanned; + } + } + + /** + * Scan a file using all applicable scanners, reporting whether a real scan + * cycle occurred. + * + * 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 + * @param monitor progress monitor to check for cancellation during scan + * @return {@link ScanOutcome} with the issues found and whether a scan + * actually ran + * @throws Exception if scan fails + */ + public ScanOutcome scanFileWithOutcome(String filePath, IProgressMonitor monitor) throws Exception { + if (filePath == null || filePath.isEmpty()) { + return new ScanOutcome(List.of(), false); + } + + // Handle null monitor (for backward compatibility if called without monitor) + if (monitor == null) { + monitor = new org.eclipse.core.runtime.NullProgressMonitor(); + } + + // 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)) { + // Nothing changed - this is NOT a fresh scan result, it's a skipped cycle. + return new ScanOutcome(List.of(), false); + } + + // Check cancellation before proceeding with expensive scan operations + if (monitor.isCanceled()) { + CxLogger.warning("[SCAN-MANAGER] Scan cancelled before starting for: " + filePath); + stateHolder.markScanComplete(filePath); + return new ScanOutcome(List.of(), false); + } + + try { + // 3. Get all scanners that support this file + + List> applicableScanners = factory.getAllSupportedScanners(filePath); + + if (applicableScanners.isEmpty()) { + + // Still update state to avoid re-checking unsupported files + stateHolder.updateStateHash(filePath, currentStateHash); + return new ScanOutcome(List.of(), false); + } + + // 4. Execute all scanners and merge results + List allIssues = new ArrayList<>(); + int successfulScanners = 0; + + for (ScannerService scanner : applicableScanners) { + // Check for cancellation before each scanner execution + if (monitor.isCanceled()) { + CxLogger.warning("[SCAN-MANAGER] Scan cancelled during scanner loop for: " + filePath); + break; + } + try { + var scanResult = scanner.scan(filePath); + List scannerResults = scanResult != null ? scanResult.getIssues() : null; + + if (scannerResults != null) { + allIssues.addAll(scannerResults); + } + successfulScanners++; + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Check for cancellation before returning/publishing results + if (monitor.isCanceled()) { + CxLogger.warning("[SCAN-MANAGER] Scan cancelled before publishing results for: " + filePath); + // Don't update state hash so file will be re-scanned when triggered again + return new ScanOutcome(List.of(), false); + } + + // 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, and don't report this as a real scan (results are unreliable). + if (successfulScanners > 0) { + stateHolder.updateStateHash(filePath, currentStateHash); + return new ScanOutcome(allIssues, true); + } + + return new ScanOutcome(List.of(), false); + } finally { + // Always release the in-flight marker so the next edit can trigger a scan. + stateHolder.markScanComplete(filePath); + } + } + + /** + * Scan a file using all applicable scanners. + * + * @param filePath Absolute file path to scan + * @param monitor progress monitor to check for cancellation during scan + * @return List of issues found by all scanners + * @throws Exception if scan fails + * @deprecated Use {@link #scanFileWithOutcome(String, IProgressMonitor)} to + * distinguish a real "zero issues" result from a skipped scan + * cycle. + */ + public List scanFile(String filePath, IProgressMonitor monitor) throws Exception { + return scanFileWithOutcome(filePath, monitor).getIssues(); + } + + /** + * Scan a file using all applicable scanners (backward-compatible overload + * without monitor). + * + * @param filePath Absolute file path to scan + * @return List of issues found by all scanners + * @throws Exception if scan fails + * @deprecated Use scanFile(String filePath, IProgressMonitor monitor) for + * cancellation support + */ + public List scanFile(String filePath) throws Exception { + return scanFile(filePath, new org.eclipse.core.runtime.NullProgressMonitor()); + } + + /** + * 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..a6e8cc48 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java @@ -0,0 +1,190 @@ +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..cf4bf153 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java @@ -0,0 +1,40 @@ +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 the CREDENTIALS_VALIDATED flag turning true - this fires regardless of which + * credential type (API key today, OAuth in future) produced the successful login, unlike + * listening for API_KEY changes directly. + * - 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 authentication just succeeded + if (Preferences.CREDENTIALS_VALIDATED.equals(event.getProperty())) { + Object newValue = event.getNewValue(); + boolean nowValidated = newValue instanceof Boolean && (Boolean) newValue; + + if (nowValidated) { + CxLogger.info(LOG_TAG + " Authentication succeeded, 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..a1537700 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java @@ -0,0 +1,322 @@ +package com.checkmarx.eclipse.devassist.configuration; + +import java.util.concurrent.CompletableFuture; + +import com.checkmarx.eclipse.common.listener.IMcpInstallCallback; +import com.checkmarx.eclipse.common.listener.IMcpUninstallCallback; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.runner.TenantSettingsProvider; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; + + +/** + * 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 listener for workspace scan trigger on authentication state change + // (when user logs in, preferences may not change, but we still need to scan) + Preferences.STORE.addPropertyChangeListener( + new com.checkmarx.eclipse.devassist.backend.AuthenticationStateListener()); + + // Register handler for post-authentication UI (welcome dialog, workspace scan) + Preferences.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler()); + + // Register handler so common-lib preference pages (e.g. + // CheckmarxPreferencePage) + // can trigger MCP install without depending on this bundle directly. + Preferences.setMcpInstallHandler(McpInstallService::installFromUi); + + // Register handler so common-lib preference pages (e.g. PreferencesPage) + // can trigger MCP uninstall on logout without depending on this bundle + // directly. + Preferences.setMcpUninstallHandler(McpInstallService::uninstallFromUi); + + 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 { + if (!Preferences.isAuthenticated()) { + CxLogger.info(LOG_TAG + " Skipping MCP auto-install: user not authenticated"); + return; + } + + String apiKey = Preferences.getApiKey(); + String additionalParams = Preferences.getAdditionalOptions(); + + 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); + } +} + + /** + * Installs MCP configuration in response to a user-initiated action (the "Install MCP" + * link on CheckmarxPreferencePage), reporting the outcome to {@code callback} instead of + * only logging it - unlike {@link #attemptAutoInstall()}, which is silent by design. + * + * @param callback notified of success or failure; may be called from a background thread + */ + public static void installFromUi(IMcpInstallCallback callback) { + CxLogger.info(LOG_TAG + " Install MCP requested from preferences page..."); + + try { + if (!Preferences.isAuthenticated()) { + callback.onFailure(PluginConstants.MCP_NOT_AUTHENTICATED_MESSAGE); + return; + } + + String apiKey = Preferences.getApiKey(); + String additionalParams = Preferences.getAdditionalOptions(); + + if (apiKey == null || apiKey.isBlank()) { + callback.onFailure(PluginConstants.MCP_NOT_AUTHENTICATED_MESSAGE); + return; + } + + boolean aiMcpEnabled; + try { + aiMcpEnabled = TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled(apiKey, additionalParams); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to check MCP server status: " + e.getMessage(), e); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + return; + } + + if (!aiMcpEnabled) { + callback.onFailure(PluginConstants.MCP_NOT_ENABLED_FOR_TENANT_MESSAGE); + return; + } + + installSilentlyAsync(apiKey).thenAccept(changed -> { + if (changed == null) { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page) failed"); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } else if (changed) { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page) succeeded"); + callback.onSuccess(); + } else { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page): already up to date"); + callback.onAlreadyUpToDate(); + } + }); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Unexpected error while installing MCP from preferences page: " + e.getMessage(), e); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } + } + + /** + * 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 in response to a user-initiated logout, reporting the + * outcome to {@code callback} instead of only logging it - unlike {@link #uninstall()}, + * which is silent by design. + * + * @param callback notified of success, not-found, or failure; may be called from a background thread + */ + public static void uninstallFromUi(IMcpUninstallCallback callback) { + CxLogger.info(LOG_TAG + " Uninstall MCP requested after logout..."); + + try { + uninstallSilentlyAsync().thenAccept(removed -> { + // Marshal callback back to UI thread - uninstallSilentlyAsync completes on a thread pool + org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { + if (removed == null) { + CxLogger.info(LOG_TAG + " Uninstall MCP (from logout) failed"); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } else if (removed) { + CxLogger.info(LOG_TAG + " Uninstall MCP (from logout) succeeded - entry removed"); + callback.onSuccess(); + } else { + CxLogger.info(LOG_TAG + " Uninstall MCP (from logout) - no entry found"); + callback.onNotFound(); + } + }); + }); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Unexpected error while uninstalling MCP from logout: " + e.getMessage(), e); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } + } + + /** + * Asynchronously uninstalls MCP configuration. + * + * @return future resolving to Boolean (true=removed, false=not found, null=error) + */ + public static CompletableFuture uninstallSilentlyAsync() { + return CompletableFuture.supplyAsync(() -> { + try { + CxLogger.info(LOG_TAG + " Background thread started, uninstalling MCP..."); + return uninstall(); + } catch (Throwable ex) { + logBackgroundFailure(ex); + return null; // null signals failure + } + }).exceptionally(ex -> { + logBackgroundFailure(ex); + return null; + }); + } + + /** + * 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..7b2dc6cc --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java @@ -0,0 +1,326 @@ +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/ignore/IgnoreEntry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreEntry.java new file mode 100644 index 00000000..a5605e91 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreEntry.java @@ -0,0 +1,252 @@ +package com.checkmarx.eclipse.devassist.ignore; + +import com.checkmarx.eclipse.devassist.utils.ScanEngine; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.List; + + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class IgnoreEntry { + public List files = new ArrayList<>(); + public ScanEngine type; // or enum + public String similarityId; + public String packageManager; + public String packageName; + public String packageVersion; + public Integer ruleId; + public String imageName; + public String imageTag; + public String severity; + public String description; + public String dateAdded; + public String title; + public String secretValue; + + public IgnoreEntry() { + } + + + + public List getFiles() { + return files; + } + + + + public void setFiles(List files) { + this.files = files; + } + + + + public ScanEngine getType() { + return type; + } + + + + public void setType(ScanEngine type) { + this.type = type; + } + + + + public String getSimilarityId() { + return similarityId; + } + + + + public void setSimilarityId(String similarityId) { + this.similarityId = similarityId; + } + + + + public String getPackageManager() { + return packageManager; + } + + + + public void setPackageManager(String packageManager) { + this.packageManager = packageManager; + } + + + + public String getPackageName() { + return packageName; + } + + + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + + + public String getPackageVersion() { + return packageVersion; + } + + + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + + + public Integer getRuleId() { + return ruleId; + } + + + + public void setRuleId(Integer ruleId) { + this.ruleId = ruleId; + } + + + + public String getImageName() { + return imageName; + } + + + + public void setImageName(String imageName) { + this.imageName = imageName; + } + + + + public String getImageTag() { + return imageTag; + } + + + + public void setImageTag(String imageTag) { + this.imageTag = imageTag; + } + + + + public String getSeverity() { + return severity; + } + + + + public void setSeverity(String severity) { + this.severity = severity; + } + + + + public String getDescription() { + return description; + } + + + + public void setDescription(String description) { + this.description = description; + } + + + + public String getDateAdded() { + return dateAdded; + } + + + + public void setDateAdded(String dateAdded) { + this.dateAdded = dateAdded; + } + + + + public String getTitle() { + return title; + } + + + + public void setTitle(String title) { + this.title = title; + } + + + + public String getSecretValue() { + return secretValue; + } + + + + public void setSecretValue(String secretValue) { + this.secretValue = secretValue; + } + + + + public static final class FileReference { + public String path; + public boolean active; + public Integer line; + public String problematicLine; + + public FileReference() { + } + + public FileReference(String relativePath, boolean b, int line, String problematicLine) { + this.path = relativePath; + this.active = b; + this.line = line; + if (problematicLine != null && !problematicLine.isEmpty()) { + this.problematicLine = problematicLine; + } + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public Integer getLine() { + return line; + } + + public void setLine(Integer line) { + this.line = line; + } + + public String getProblematicLine() { + return problematicLine; + } + + public void setProblematicLine(String problematicLine) { + this.problematicLine = problematicLine; + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreFileManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreFileManager.java new file mode 100644 index 00000000..1641c276 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreFileManager.java @@ -0,0 +1,711 @@ +package com.checkmarx.eclipse.devassist.ignore; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.core.resources.IProject; +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.swt.widgets.Display; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Manages the ignore file (.checkmarxIgnored) within the project's workspace. + * Handles reading, writing, and updating ignore entries. + * Provides methods to ignore issues and update temporary ignore lists. + */ +public final class IgnoreFileManager { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static boolean skipFileWatcherForTests = false; + private static final Map INSTANCES = new HashMap<>(); + + private final IProject project; + private String workspacePath = ""; + private String workspaceRootPath = ""; + private Map ignoreData = new HashMap<>(); + private final Map scannedFileMap = new HashMap<>(); + private Map previousIgnoreData = new HashMap<>(); + private final List listeners = Collections.synchronizedList(new ArrayList<>()); + // Captured so dispose() can unregister it - startFileWatcher() previously + // passed an anonymous listener straight to addResourceChangeListener() with + // no reference kept anywhere, so a closed project's manager (and its + // workspace-level listener, which keeps firing on every future resource + // change regardless of which project changed) could never be unregistered. + private IResourceChangeListener resourceChangeListener; + + public interface IgnoreListener { + void onIgnoreUpdated(); + } + + public static synchronized IgnoreFileManager getInstance(IProject project) { + if (!INSTANCES.containsKey(project)) { + INSTANCES.put(project, new IgnoreFileManager(project)); + } + return INSTANCES.get(project); + } + + /** + * Evicts and disposes the cached IgnoreFileManager for a closed project - + * unregisters its workspace-level resource-change listener and drops it + * from INSTANCES. Without this, every project ever opened in a session + * stays in INSTANCES forever with its listener still firing on every future + * workspace resource change, even for projects that no longer exist. + * + * @param project the project that is closing + */ + public static synchronized void dispose(IProject project) { + IgnoreFileManager manager = INSTANCES.remove(project); + if (manager != null) { + manager.disposeInternal(); + } + } + + private void disposeInternal() { + if (resourceChangeListener != null) { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener); + resourceChangeListener = null; + } + listeners.clear(); + } + + public IgnoreFileManager(IProject project) { + this.project = project; + String basePath = project.getLocation().toOSString(); + if (basePath != null && !basePath.isEmpty()) { + this.workspaceRootPath = basePath; + this.workspacePath = Paths.get(basePath, ".checkmarx").toString(); + ensureIgnoreFileExists(); + loadIgnoreData(); + this.previousIgnoreData = copyIgnoreData(ignoreData); + } + if (!skipFileWatcherForTests) { + startFileWatcher(); + } + } + + private void startFileWatcher() { + org.eclipse.core.resources.IFile ignoreIFile = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(getIgnoreFilePath().toString())); + if (ignoreIFile == null) { + return; + } + resourceChangeListener = (IResourceChangeListener) event -> { + IResourceDelta delta = event.getDelta(); + if (delta == null) { + return; + } + IResourceDelta ignoreDelta = delta.findMember(ignoreIFile.getFullPath()); + if (ignoreDelta != null && (ignoreDelta.getFlags() & IResourceDelta.CONTENT) != 0) { + // Marshal onto the UI thread: ignoreData is a plain HashMap and every + // existing reader/writer (isIgnored, addIgnoredEntry, reviveEntry) already + // only ever runs on the SWT UI thread; this keeps that invariant intact + // instead of introducing a background-thread race on the map. + Display display = Display.getDefault(); + if (display != null && !display.isDisposed()) { + display.asyncExec(this::handleFileChange); + } + } + }; + ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener, IResourceChangeEvent.POST_CHANGE); + } + + public void updateIgnoreData(String vulnerabilityKey, IgnoreEntry newData) { + if (newData == null) return; + ignoreData.put(vulnerabilityKey, newData); + saveIgnoreFile(); + updateIgnoreTempList(); + } + + /** + * Ensures the ignored file exists; + * Creates it if missing. + * Logs a warning if creation fails. + */ + + private void ensureIgnoreFileExists() { + try { + Path dir = Paths.get(workspacePath); + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } + Path ignoreFile = getIgnoreFilePath(); + if (!Files.exists(ignoreFile)) { + Files.write(ignoreFile, "{}\n".getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + CxLogger.error("Failed to ensure ignore file exists", e); + } + } + + public void loadIgnoreData() { + loadIgnoreDataInternal(); + } + + /** + * Loads ignore data from disk into {@link #ignoreData}. + * + * @return true if the file was read and parsed successfully (or genuinely + * doesn't exist yet, which is a legitimate empty state), false if a + * read/parse error occurred. On false, {@link #ignoreData} is left + * untouched - a transient read error (e.g. the file watcher observing + * our own write mid-flight) must never be treated as "the file is now + * empty", or every previously-ignored entry appears to vanish from + * the Ignored Findings window and reappears as an active finding in + * the editor until the next successful reload. + */ + private boolean loadIgnoreDataInternal() { + Path ignoreFile = getIgnoreFilePath(); + if (!Files.exists(ignoreFile)) { + CxLogger.info(String.format("RTS-Ignore: Ignore file doesn't exist: %s", ignoreFile)); + ignoreData = new HashMap<>(); + return true; + } + try (InputStream inputStream = Files.newInputStream(ignoreFile)) { + ObjectMapper mapper = new ObjectMapper(); + Map data = mapper.readValue(inputStream, + new TypeReference>() { + }); + ignoreData.clear(); + ignoreData.putAll(data); + return true; + } catch (IOException e) { + CxLogger.warning("RTS-Ignore: Failed to read ignore file (keeping previous in-memory state): " + + ignoreFile + " - " + e.getMessage()); + return false; + } + } + + + /** + * Returns all ignore entries. + * + * @return list of ignore entries. + */ + public List getAllIgnoreEntries() { + return new ArrayList<>(ignoreData.values()); + } + + /** + * Returns the ignore data map for this project. + * This is an instance method to ensure project-level isolation. + * + * @return the ignore data map + */ + public Map getIgnoreData() { + return ignoreData; + } + + /** + * Checks if a ScanIssue is ignored based on similarity ID. + * + * @param similarityId The similarity ID of the issue + * @return true if the issue is ignored, false otherwise + */ + public boolean isIgnored(String similarityId) { + if (similarityId == null || similarityId.isEmpty()) { + return false; + } + return ignoreData.containsKey(similarityId); + } + + /** + * Saves the current ignore data to the ignore file. + * Writes the ignore data as formatted JSON to the file specified by {@link #getIgnoreFilePath()}. + * Creates a new file if it doesn't exist, or truncates the existing file. + * Notifies all subscribers about the update. + * Logs a warning if saving fails. + */ + private void saveIgnoreFile() { + try { + String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(ignoreData); + Path ignoreFilePath = getIgnoreFilePath(); + writeAtomically(ignoreFilePath, json); + refreshFileInWorkspace(ignoreFilePath); + notifyListeners(); + } catch (IOException e) { + CxLogger.warning("RTS-Ignore: Exception occurred while adding ignore entry into file: " + e.getMessage()); + } + } + + /** + * Forces Eclipse's resource model to pick up a file we just wrote directly + * via java.nio (writeAtomically bypasses IFile/IResource entirely). Without + * this, the Ignored Findings view (or any other resource-change listener) + * only learns about the change once native/polling workspace refresh gets + * around to it - which can be disabled or delayed in managed/enterprise + * Eclipse installs - leaving a window where the on-disk file and Eclipse's + * view of it disagree. + */ + private void refreshFileInWorkspace(Path filePath) { + try { + org.eclipse.core.resources.IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(filePath.toString())); + if (file != null) { + file.refreshLocal(org.eclipse.core.resources.IResource.DEPTH_ZERO, null); + } + } catch (org.eclipse.core.runtime.CoreException e) { + CxLogger.warning("RTS-Ignore: Failed to refresh workspace resource for " + filePath + ": " + e.getMessage()); + } + } + + /** + * Writes content to the target path atomically: write to a sibling temp + * file, then move it into place with ATOMIC_MOVE. + * + * Files.writeString(..., TRUNCATE_EXISTING) truncates the file to zero + * length before writing the new content, which is observable as two + * separate filesystem events (truncate, then write). Eclipse's resource + * watcher (native hooks or polling refresh) can fire on that intermediate, + * momentarily-empty state, causing a concurrent loadIgnoreData() call to + * throw MismatchedInputException on a file that was never actually + * corrupted on disk - just read mid-write. An atomic rename never exposes + * that intermediate state: a reader always sees either the complete old + * content or the complete new content. + */ + private void writeAtomically(Path target, String content) throws IOException { + Path tempFile = target.resolveSibling(target.getFileName().toString() + "." + UUID.randomUUID() + ".tmp"); + try { + Files.writeString(tempFile, content, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + try { + Files.move(tempFile, target, java.nio.file.StandardCopyOption.ATOMIC_MOVE, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (java.nio.file.AtomicMoveNotSupportedException e) { + // Some filesystems (e.g. certain network drives) don't support atomic + // moves across the temp/target pair - fall back to a plain replace. + Files.move(tempFile, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(tempFile); + } + } + + public void addListener(IgnoreListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(IgnoreListener listener) { + listeners.remove(listener); + } + + private void notifyListeners() { + for (IgnoreListener listener : new ArrayList<>(listeners)) { + try { + listener.onIgnoreUpdated(); + } catch (Exception e) { + CxLogger.warning("Error notifying ignore listener: " + e.getMessage()); + } + } + } + + + /** + * Updates the temporary ignore list file based on active ignore entries. + * Creates a list of temporary items from active ignore entries, categorized by their type (OSS, Secrets, IAC, Containers, ASCA). + * For each entry type: + * - OSS: adds package manager, name and version + * - Secrets: adds package name and secret value + * - IAC: adds package name and similarity ID + * - Containers: adds image name and image tag + * - ASCA: adds file name, line number and rule ID for each active file + * The temporary list is then saved to a JSON file at the path specified by {@link #getTempListPath()}. + */ + public void updateIgnoreTempList() { + List tempList = new ArrayList<>(); + CxLogger.info(String.format("RTS-Ignore: [TEMP_LIST_UPDATE] Updating temp list with %d ignore entries", ignoreData.size())); + + for (IgnoreEntry entry : ignoreData.values()) { + boolean hasActive = entry.files.stream().anyMatch(f -> f.active); + if (!hasActive) { + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Skipping entry with no active files: " + entry.getPackageName()); + continue; + } + switch (entry.type) { + case OSS: + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Adding OSS entry: " + entry.getPackageName()); + tempList.add(TempItem.forOss(entry.packageManager, entry.packageName, entry.packageVersion)); + break; + case SECRETS: + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Adding SECRETS entry: " + entry.getPackageName()); + tempList.add(TempItem.forSecret(entry.packageName, entry.secretValue)); + break; + case IAC: + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Adding IAC entry: " + entry.getPackageName()); + tempList.add(TempItem.forIac(entry.packageName, entry.similarityId)); + break; + case CONTAINERS: + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Adding CONTAINERS entry: " + entry.getImageName()); + tempList.add(TempItem.forContainer(entry.imageName, entry.imageTag)); + break; + case ASCA: + for (IgnoreEntry.FileReference file : entry.files) { + if (!file.active) continue; + String originalPath = Paths.get(workspaceRootPath, file.path).toAbsolutePath().toString(); + String scannedTempPath = scannedFileMap.getOrDefault(originalPath, originalPath); + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Adding ASCA entry: " + entry.getPackageName() + " at line " + file.line); + tempList.add(TempItem.forAsca( + Paths.get(scannedTempPath).getFileName().toString(), + file.line, + entry.ruleId + )); + } + break; + default: + break; + } + } + + try { + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Writing " + tempList.size() + " items to temp list file"); + String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tempList); + Path tempListPath = getTempListPath(); + writeAtomically(tempListPath, json); + refreshFileInWorkspace(tempListPath); + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE_SUCCESS] Temp list updated successfully with " + tempList.size() + " items"); + + // If tempList is empty, verify the file is actually empty + if (tempList.isEmpty()) { + CxLogger.info("RTS-Ignore: [TEMP_LIST_UPDATE] Temp list is empty - file should contain empty array []"); + } + } catch (IOException e) { + CxLogger.error("RTS-Ignore: [TEMP_LIST_UPDATE_ERROR] Failed to update temp list: " + e.getMessage(), e); + } + } + + /** + * Revives a previously ignored package by setting all its file references to inactive. + * This makes the vulnerability visible again in future scans. + * + * @param entryToRevive The unique key identifying the ignored package + * @return true if the package was found and revived, false otherwise + */ + public boolean reviveEntry(IgnoreEntry entryToRevive) { + boolean success = reviveEntryInternal(entryToRevive); + if (success) { + saveIgnoreFile(); + updateIgnoreTempList(); + } + return success; + } + + /** + * Internal method to revive an entry without saving to disk. + * Used by batch operations that need to revive multiple entries before saving once. + * + * @param entryToRevive The unique key identifying the ignored package + * @return true if the package was found and revived, false otherwise + */ + private boolean reviveEntryInternal(IgnoreEntry entryToRevive) { + String entryKey = ignoreData.entrySet().stream() + .filter(e -> matchesEntry(e.getValue(), entryToRevive)) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + if (entryKey == null) { + CxLogger.warning("RTS-Ignore: Entry not found in ignoreData map"); + return false; + } + IgnoreEntry actualEntry = ignoreData.get(entryKey); + String packageName = entryToRevive.getPackageName(); + for (IgnoreEntry.FileReference file : actualEntry.getFiles()) { + file.active = false; + } + CxLogger.info("RTS-Ignore: Revived package: " + packageName); + return true; + } + + + public Path getIgnoreFilePath() { + return Paths.get(workspacePath, ".checkmarxIgnored"); + } + + /** + * Deletes the ignore file (.checkmarxIgnored) and the temporary ignore list file. + * Called when the user no longer has a valid license (platform-only license). + * This ensures that ignored findings are cleared when the feature is not available. + */ + public void deleteIgnoreFiles() { + try { + Path ignoreFilePath = getIgnoreFilePath(); + if (Files.exists(ignoreFilePath)) { + Files.delete(ignoreFilePath); + CxLogger.info("RTS-Ignore: Deleted ignore file at " + ignoreFilePath); + } + + Path tempListPath = Paths.get(workspacePath, ".checkmarxIgnoredTempList.json"); + if (Files.exists(tempListPath)) { + Files.delete(tempListPath); + CxLogger.info("RTS-Ignore: Deleted temp list file at " + tempListPath); + } + + // Clear in-memory data + ignoreData.clear(); + previousIgnoreData.clear(); + + // Notify listeners that ignore data has changed + notifyListeners(); + } catch (IOException e) { + CxLogger.error("RTS-Ignore: Failed to delete ignore files", e); + } + } + + /** + * Returns the path to the temporary ignore list. + * Creates the file if it doesn't exist. + * + * @return path to the temporary ignore list. + * + */ + public Path getTempListPath() { + Path tempListPath = Paths.get(workspacePath, ".checkmarxIgnoredTempList.json"); + if (Files.exists(tempListPath)) { + try { + // Validate it's a valid JSON array + if (Files.readString(tempListPath).trim().isEmpty()) { + Files.writeString(tempListPath, "[]", StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING); + } + return tempListPath; + } catch (IOException e) { + CxLogger.error("Failed to validate temp list: " + tempListPath, e); + createEmptyTempList(tempListPath); + } + } else { + createEmptyTempList(tempListPath); + } + return tempListPath; // Guaranteed to exist and contain [] + } + + private void createEmptyTempList(Path tempListPath) { + try { + Files.createDirectories(tempListPath.getParent()); + Files.writeString(tempListPath, "[]", StandardCharsets.UTF_8); + CxLogger.info(String.format("RTS-Ignore: Created empty temp list at %s", tempListPath)); + } catch (IOException e) { + CxLogger.error("Failed to create empty temp list", e); + } + } + + /** + * normalizes the given file path to be relative to the project's workspace root. + * + * @param filePath + * @return + */ + public String normalizePath(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return ""; + } + try { + return Path.of(workspaceRootPath) + .relativize(Paths.get(filePath)) + .toString() + .replace("\\", "/"); + } catch (Exception e) { + // Malformed/foreign path (e.g. a different filesystem root) - fall back to + // the raw path rather than throwing, so callers that iterate over many + // issues (e.g. "ignore all of this type") don't abort partway through. + return filePath.replace("\\", "/"); + } + } + + private void handleFileChange() { + CxLogger.info("RTS-Ignore: [FILE_WATCHER] File change detected in .checkmarxIgnored"); + CxLogger.info("RTS-Ignore: [FILE_WATCHER] Current ignoreData size: " + ignoreData.size()); + refreshFromDisk(); + CxLogger.info("RTS-Ignore: [FILE_WATCHER] After refresh, ignoreData size: " + ignoreData.size()); + } + + /** + * Re-reads .checkmarxIgnored from disk and reconciles derived state + * (.checkmarxIgnoredTempList.json, listeners) against it. + * + * This is the same reconciliation the file watcher triggers on a detected + * change, exposed so callers that can't rely on Eclipse's resource-change + * notification firing reliably for every edit (e.g. a genuinely external + * edit made while native/polling refresh is disabled, or before a + * time-sensitive read like kicking off a scan) can force it explicitly. + * + * @return true if the file was read successfully (or doesn't exist), + * false if a transient read/parse error occurred - in which case + * in-memory state was left untouched and no reconciliation ran. + */ + public boolean refreshFromDisk() { + Map beforeIgnoreData = copyIgnoreData(ignoreData); + if (!loadIgnoreDataInternal()) { + // Transient read failure (e.g. watcher observed a mid-write state) - + // ignoreData was left untouched, so there is nothing real to react to + // this cycle. Bail out without cascading a false "everything + // deactivated" into detectAndHandleActiveChanges()/updateIgnoreTempList(). + return false; + } + detectAndHandleActiveChanges(); + previousIgnoreData = copyIgnoreData(ignoreData); + + // Only notify listeners if the ignore data actually changed + if (!ignoreDataEquals(beforeIgnoreData, ignoreData)) { + notifyListeners(); + } + return true; + } + + private boolean ignoreDataEquals(Map map1, Map map2) { + if (map1.size() != map2.size()) { + return false; + } + for (String key : map1.keySet()) { + if (!map2.containsKey(key)) { + return false; + } + // Compare JSON serialization for deep equality + try { + String json1 = MAPPER.writeValueAsString(map1.get(key)); + String json2 = MAPPER.writeValueAsString(map2.get(key)); + if (!json1.equals(json2)) { + return false; + } + } catch (IOException e) { + return false; + } + } + return true; + } + + + private void detectAndHandleActiveChanges() { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Detecting active changes in ignore data"); + List previousActiveFiles = getActiveFilesList(previousIgnoreData); + List currentActiveFiles = getActiveFilesList(ignoreData); + + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Previous active files: " + previousActiveFiles.size()); + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Current active files: " + currentActiveFiles.size()); + + List deactivatedFiles = previousActiveFiles.stream() + .filter(prev -> currentActiveFiles.stream() + .noneMatch(cur -> cur.packageKey.equals(prev.packageKey) && cur.path.equals(prev.path))) + .collect(Collectors.toList()); + + if (!deactivatedFiles.isEmpty()) { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Found " + deactivatedFiles.size() + " deactivated files"); + for (ActiveFile f : deactivatedFiles) { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Removing deactivated entry: " + f.packageKey + " at " + f.path); + removeIgnoredEntryWithoutTempUpdate(f.packageKey, f.path); + } + updateIgnoreTempList(); + } + + // Remove entries where all files are inactive + List keysToRemove = new ArrayList<>(); + for (Map.Entry entry : ignoreData.entrySet()) { + boolean hasActive = entry.getValue().files.stream().anyMatch(f -> f.active); + if (!hasActive) { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Entry has no active files, marking for removal: " + entry.getKey()); + keysToRemove.add(entry.getKey()); + } + } + if (!keysToRemove.isEmpty()) { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Removing " + keysToRemove.size() + " entries with no active files"); + for (String key : keysToRemove) { + CxLogger.info("RTS-Ignore: [DETECT_CHANGES] Removing key: " + key); + ignoreData.remove(key); + } + saveIgnoreFile(); + } + + CxLogger.info("RTS-Ignore: [DETECT_CHANGES_COMPLETE] After changes, ignoreData size: " + ignoreData.size()); + } + + private static final class ActiveFile { + final String packageKey; + final String path; + + ActiveFile(String packageKey, String path) { + this.packageKey = packageKey; + this.path = path; + } + } + + private List getActiveFilesList(Map data) { + List result = new ArrayList<>(); + for (Map.Entry e : data.entrySet()) { + for (IgnoreEntry.FileReference fileRef : e.getValue().files) { + if (fileRef.active) { + result.add(new ActiveFile(e.getKey(), fileRef.path)); + } + } + } + return result; + } + + private void removeIgnoredEntryWithoutTempUpdate(String packageKey, String filePath) { + IgnoreEntry entry = ignoreData.get(packageKey); + if (entry == null) return; + entry.files.removeIf(fileRef -> fileRef.path.equals(filePath)); + if (entry.files.isEmpty()) { + ignoreData.remove(packageKey); + } + saveIgnoreFile(); + + } + + private Map copyIgnoreData(Map src) { + // Deep copy via JSON round-trip + try { + String json = MAPPER.writeValueAsString(src); + return MAPPER.readValue(json, new TypeReference>() { + }); + } catch (IOException e) { + CxLogger.error("Failed to deep copy ignoreData, falling back to shallow copy", e); + return new HashMap<>(src); + } + } + + // Helper method to match entries by properties + public boolean matchesEntry(IgnoreEntry entry1, IgnoreEntry entry2) { + if (entry1.getType() != entry2.getType()) return false; + // Match based on type-specific unique identifiers + switch (entry1.getType()) { + case OSS: + return Objects.equals(entry1.getPackageName(), entry2.getPackageName()) && + Objects.equals(entry1.getPackageVersion(), entry2.getPackageVersion()) && + Objects.equals(entry1.getPackageManager(), entry2.getPackageManager()); + case CONTAINERS: + return Objects.equals(entry1.getImageName(), entry2.getImageName()) && + Objects.equals(entry1.getImageTag(), entry2.getImageTag()); + case SECRETS: + return Objects.equals(entry1.getPackageName(), entry2.getPackageName()) && + Objects.equals(entry1.getSecretValue(), entry2.getSecretValue()); + case IAC: + return Objects.equals(entry1.getPackageName(), entry2.getPackageName()) && + Objects.equals(entry1.getSimilarityId(), entry2.getSimilarityId()); + case ASCA: + return Objects.equals(entry1.getPackageName(), entry2.getPackageName()) && + Objects.equals(entry1.getRuleId(), entry2.getRuleId()); + default: + return false; + } + } + + /** + * Saves the current ignore data to disk. + * This is a public wrapper for the private saveIgnoreFile method. + * Used when ignore data is modified directly (e.g., line number updates). + */ + public void saveIgnoreDataToDisk() { + saveIgnoreFile(); + updateIgnoreTempList(); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreManager.java new file mode 100644 index 00000000..a06061bc --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/IgnoreManager.java @@ -0,0 +1,963 @@ +package com.checkmarx.eclipse.devassist.ignore; +import org.eclipse.core.resources.IProject; +import com.checkmarx.eclipse.common.utils.CxLogger; + + + +import java.nio.file.Paths; +import java.time.Instant; +import java.util.*; + +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.QUICK_FIX; +import static java.lang.String.format; + +/** + * Manages the ignore file (.checkmarxIgnored) within the project's workspace. + * Handles reading, writing, and updating ignore entries. + * Monitors the ignore file for changes and updates internal state accordingly. + * Provides methods to ignore issues and update temporary ignore lists. + */ +public final class IgnoreManager { + // Per-project cache (mirrors IgnoreFileManager's pattern) - a single static + // "instance" field replaced on every call with a different project used to + // silently discard whichever project's IgnoreManager wasn't the most + // recently requested. Nothing currently holds a reference across an async + // boundary, but any caller that does would then act on the wrong project's + // ignore state the moment another project's lookup swapped the field. + private static final Map INSTANCES = new HashMap<>(); + private final org.eclipse.core.resources.IProject project; + private final ProblemHolderService problemHolder; + private final IgnoreFileManager ignoreFileManager; + + public IgnoreManager(org.eclipse.core.resources.IProject project) { + this.project = project; + this.problemHolder = ProblemHolderService.getInstance(project); + this.ignoreFileManager = IgnoreFileManager.getInstance(project); + } + + public static synchronized IgnoreManager getInstance(org.eclipse.core.resources.IProject project) { + return INSTANCES.computeIfAbsent(project, IgnoreManager::new); + } + + /** + * Evicts the cached IgnoreManager for a closed project, so a stale + * per-project instance can never be resolved (or accumulate indefinitely) + * once the project is gone. Holds no listeners/resources of its own beyond + * the ProblemHolderService/IgnoreFileManager references it wraps - those are + * disposed independently - so simply dropping the reference is sufficient. + * + * @param project the project that is closing + */ + public static synchronized void dispose(org.eclipse.core.resources.IProject project) { + INSTANCES.remove(project); + } + + /** + * Adds an entry to the ignore file for this specific occurrence only (this + * file). If the same vulnerability (by key) is already ignored elsewhere + * (e.g. via "ignore all" or a previous "ignore this" in another file), the + * existing entry's file list is merged into rather than replaced, so + * ignoring in one file never clears an ignore already recorded for another. + * + * @param issueToIgnore The scan issue to ignore + * @param clickId The ID of the clicked action or vulnerability, used to retrieve additional details + */ + public void addIgnoredEntry(ScanIssue issueToIgnore, String clickId) { + CxLogger.info(String.format("RTS-Ignore: Adding ignore entry for issue: %s", issueToIgnore.getTitle())); + + String vulnerabilityKey = createJsonKeyForIgnoreEntry(issueToIgnore, clickId); + if (vulnerabilityKey.isEmpty()) { + CxLogger.info("RTS-Ignore: Ignoring vulnerability failed. Vulnerability key is empty."); + return; + } + IgnoreEntry existingEntry = ignoreFileManager.getIgnoreData().get(vulnerabilityKey); + IgnoreEntry ignoreEntry; + if (existingEntry != null) { + ignoreEntry = existingEntry; + upsertFileReference(ignoreEntry, issueToIgnore, resolveVulnerability(issueToIgnore, clickId)); + } else { + // Convert ScanIssue → IgnoreEntry (includes this occurrence's file reference) + ignoreEntry = buildIgnoreEntry(issueToIgnore, clickId); + } + if (Objects.isNull(ignoreEntry)) { + // Notification removed: use Eclipse MessageDialog instead + return; + } + CxLogger.info(String.format("RTS-Ignore: Ignoring %s", vulnerabilityKey)); + ignoreFileManager.updateIgnoreData(vulnerabilityKey, ignoreEntry); + // For a ScanIssue grouping several vulnerabilities on one line (ASCA/IAC), the cached + // ScanIssue in ProblemHolderService/CxFindingsView still holds every vulnerability in + // the group - including the one just ignored - until the next scan rebuilds it via the + // adaptor's per-vulnerability filtering. Without an immediate rescan, isIgnored(issue) + // checks made against that stale cached issue (e.g. ProblemDecorator.decorateEditor, + // CxFindingsView.refreshTreeWithFilter) can resolve to the just-ignored vulnerability + // and incorrectly treat the WHOLE line as ignored, hiding any other still-active + // vulnerability on it until something else happens to trigger a rescan. + triggerRescanForFile(issueToIgnore.getFilePath()); +// showIgnoreSuccessNotification(project, issueToIgnore, vulnerabilityKey); + CxLogger.info(String.format("RTS-Ignore: Successfully added ignore entry for issue: %s", issueToIgnore.getTitle())); + } + + /** + * Adds (or reactivates) a single file reference on an existing ignore + * entry for the given issue's file, without touching any other file's + * reference already recorded on that entry. + *

    + * ASCA/IAC ignore keys deliberately omit the line number (see + * {@link #createJsonKeyForIgnoreEntry}), so a single key legitimately covers every + * occurrence of that rule in the file - matching an existing {@link IgnoreEntry.FileReference} + * by path alone would find *any* prior occurrence recorded under this key and overwrite its + * line/problematicLine, silently losing track of it instead of recording this (different) + * occurrence separately. Matching by path + problematicLine identifies the SAME occurrence + * (e.g. re-ignoring after a revive), while a different problematicLine is always treated as + * a new occurrence to add. + */ + private void upsertFileReference(IgnoreEntry entry, ScanIssue issue, Vulnerability vulnerability) { + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + String path = ignoreFileManager.normalizePath(issue.getFilePath()); + int line = issue.getLocations().get(0).getLine(); + String problematicLine = vulnerability != null && vulnerability.getProblematicLine() != null + ? vulnerability.getProblematicLine() : ""; + if (entry.files == null) { + entry.files = new ArrayList<>(); + } + for (IgnoreEntry.FileReference ref : entry.files) { + boolean sameOccurrence = path.equals(ref.getPath()) + && (problematicLine.isEmpty() + ? ref.getProblematicLine() == null + : problematicLine.equals(ref.getProblematicLine())); + if (sameOccurrence) { + ref.setActive(true); + ref.setLine(line); + return; + } + } + entry.files.add(new IgnoreEntry.FileReference(path, true, line, problematicLine)); + } + + + /** + * Adds an entry to the ignore file for all occurrences of the specified issue. + * This method performs the following steps: + * 1. Creates a vulnerability key for the given issue + * 2. Gets all issues from the problem holder and creates a deep copy + * 3. Creates an ignore entry for the issue + * 4. Iterates through all issues and adds matching ones to the ignore list + * 5. Updates the ignore file and removes the issues from the problem holder + * + * @param issueToIgnore The scan issue to ignore across all files + * @param clickId The ID that was clicked to trigger the ignore action + */ + public void addAllIgnoredEntry(ScanIssue issueToIgnore, String clickId) { + try { + CxLogger.info(String.format("RTS-Ignore: Adding ignore entry for issue: %s", issueToIgnore.getTitle())); + String vulnerabilityKey = createJsonKeyForIgnoreEntry(issueToIgnore, clickId); + CxLogger.info("RTS-Ignore: Ignoring all vulnerabilities for: " + vulnerabilityKey); + if (vulnerabilityKey.isEmpty()) { + CxLogger.info("RTS-Ignore: Ignoring all vulnerabilities failed. Vulnerability key is empty."); + return; + } + IgnoreEntry ignoreEntry = buildIgnoreEntry(issueToIgnore, clickId); + if (Objects.isNull(ignoreEntry)) { + // Notification removed: use Eclipse MessageDialog instead + return; + } + // Best-effort: pick up every other currently-known occurrence of this + // vulnerability from the problem holder. This may legitimately be empty + // (e.g. a file whose scan was skipped as unchanged since the holder was + // last populated) - that must NOT stop the clicked occurrence itself from + // being ignored, so no early-return on an empty/no-match snapshot here. + Map> allIssues = new HashMap<>(); + for (Map.Entry> entry : problemHolder.getAllScanIssues().entrySet()) { + allIssues.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + List fileRefs = new ArrayList<>(); + for (List issues : allIssues.values()) { // Safe: allIssues never mutates + for (ScanIssue issue : issues) { + // Issues missing a location (e.g. malformed/partial entries) can't be + // matched by key reliably and have no line to record - skip them + // instead of letting a single bad issue abort ignoring every match. + if (issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + continue; + } + if (!createJsonKeyForIgnoreEntry(issue, clickId).equals(vulnerabilityKey)) { + continue; + } + fileRefs.add(new IgnoreEntry.FileReference( + ignoreFileManager.normalizePath(issue.getFilePath()), + true, + issue.getLocations().get(0).getLine(), "")); + } + } + // Guarantee the clicked occurrence itself is covered even if it was + // somehow absent from the problemHolder snapshot above. + String clickedPath = ignoreFileManager.normalizePath(issueToIgnore.getFilePath()); + boolean clickedCovered = fileRefs.stream().anyMatch(ref -> clickedPath.equals(ref.getPath())); + if (!clickedCovered && issueToIgnore.getLocations() != null && !issueToIgnore.getLocations().isEmpty()) { + fileRefs.add(new IgnoreEntry.FileReference( + clickedPath, true, issueToIgnore.getLocations().get(0).getLine(), "")); + } + ignoreEntry.files = fileRefs; + ignoreFileManager.updateIgnoreData(vulnerabilityKey, ignoreEntry); + CxLogger.info(String.format("RTS-Ignore: Successfully added ignore entry for issue: %s", issueToIgnore.getTitle())); + } catch (Exception e) { + CxLogger.warning("RTS-Ignore: Failed to add ignore-all entry for issue: " + + (issueToIgnore != null ? issueToIgnore.getTitle() : "unknown") + " - " + e.getMessage()); + } + } + + + /** + * Revives a single ignored vulnerability. + * Shows a notification with an "Undo" option that allows the user to restore the ignored state. + * The revive operation is performed first, then the user can undo it if desired. + * This follows the same pattern as the VS Code extension's revivePackage method. + * + * @param entryToRevive The ignore entry to revive + */ + public void reviveSingleEntry(IgnoreEntry entryToRevive) { + CxLogger.info(format("RTS-Ignore: Reviving entry: %s", entryToRevive.getPackageName())); + Map ignoredEntries = new HashMap<>(ignoreFileManager.getIgnoreData()); + + // Count active files before reviving + int fileCount = (int) entryToRevive.files.stream() + .filter(f -> f.active) + .count(); + // Perform the revive operation (sets all file references to inactive) + boolean success = ignoreFileManager.reviveEntry(entryToRevive); + if (!success) { + // Notification removed: use Eclipse MessageDialog instead + CxLogger.warning(format("RTS-Ignore: Failed to revive entry: %s", entryToRevive.getPackageName())); + return; + } + // Trigger rescan for affected files + triggerRescanForEntry(entryToRevive); + // Show notification with undo option +// showReviveUndoNotification(entryToRevive, fileCount, ignoredEntries); + CxLogger.info(format("RTS-Ignore: Successfully revived entry: %s", entryToRevive.getPackageName())); + } + + /** + * Revives multiple ignored vulnerabilities in bulk. + * Batches all the revive operations together and saves to disk once at the end, + * then triggers rescans for all affected files. + * + * @param entriesToRevive List of entries to revive + */ + public void reviveMultipleEntries(List entriesToRevive) { + if (entriesToRevive == null || entriesToRevive.isEmpty()) { + CxLogger.warning("RTS-Ignore: No package keys provided for bulk revive"); + return; + } + CxLogger.info(format("RTS-Ignore: Bulk reviving %d entries", entriesToRevive.size())); + + int successCount = 0; + int totalFileCount = 0; + List failedIgnoreEntry = new ArrayList<>(); + List revivedEntries = new ArrayList<>(); + + // Revive all entries in memory first (without individual saves) + for (IgnoreEntry entryToRevive : entriesToRevive) { + int fileCount = (int) entryToRevive.files.stream() + .filter(f -> f.active) + .count(); + // Use internal revive that doesn't save to disk + boolean success = reviveSingleEntryInternal(entryToRevive); + if (success) { + successCount++; + totalFileCount += fileCount; + revivedEntries.add(entryToRevive); + CxLogger.info(String.format("RTS-Ignore: Successfully revived in memory: %s", entryToRevive.getPackageName())); + } else { + failedIgnoreEntry.add(entryToRevive); + CxLogger.warning(String.format("RTS-Ignore: Failed to revive entry: %s", entryToRevive.getPackageName())); + } + } + + // Save to disk once after all revives + if (successCount > 0) { + ignoreFileManager.saveIgnoreDataToDisk(); + CxLogger.info(format("RTS-Ignore: Saved all %d revived entries to disk", successCount)); + } + + // Trigger rescans for all revived entries + for (IgnoreEntry entry : revivedEntries) { + triggerRescanForEntry(entry); + } + + // Log summary + String message; + if (successCount == 1) { + message = String.format("Revived 1 vulnerability in %d file%s", + totalFileCount, totalFileCount == 1 ? "" : "s"); + } else if (successCount > 1) { + message = String.format("Revived %d vulnerabilities in %d file%s", + successCount, totalFileCount, totalFileCount == 1 ? "" : "s"); + } else { + message = "No vulnerabilities revived"; + } + if (!failedIgnoreEntry.isEmpty()) { + message += String.format(" (%d failed)", failedIgnoreEntry.size()); + } + CxLogger.info(format("RTS-Ignore: Bulk revive summary: %s", message)); + } + + /** + * Internal method to revive a single entry without saving to disk. + * Used by bulk operations that batch multiple revives before a single save. + * + * @param entryToRevive The ignore entry to revive + * @return true if the entry was found and revived, false otherwise + */ + private boolean reviveSingleEntryInternal(IgnoreEntry entryToRevive) { + CxLogger.info(format("RTS-Ignore: Reviving entry (internal): %s", entryToRevive.getPackageName())); + + // Find the entry in ignoreData map by matching properties + String entryKey = ignoreFileManager.getIgnoreData().entrySet().stream() + .filter(e -> ignoreFileManager.matchesEntry(e.getValue(), entryToRevive)) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + + if (entryKey == null) { + CxLogger.warning(format("RTS-Ignore: Entry not found in ignoreData: %s", entryToRevive.getPackageName())); + return false; + } + + IgnoreEntry actualEntry = ignoreFileManager.getIgnoreData().get(entryKey); + // Set all file references to inactive + for (IgnoreEntry.FileReference file : actualEntry.getFiles()) { + file.active = false; + } + CxLogger.info(format("RTS-Ignore: Marked all files as inactive for: %s", entryToRevive.getPackageName())); + return true; + } + + /** + * Checks whether the given scan issue is currently ignored: the composite + * vulnerability key must match AND the entry must have an active file + * reference for this issue's specific file. This makes "ignore this" scoped + * to the file it was ignored from (an entry with one active file reference + * only hides the finding in that file), while "ignore all of this type" + * (which records an active reference per matching file) hides it everywhere, + * matching the {@code .checkmarxIgnored} files array's path/active/line + * per-occurrence tracking. + * + * @param issue The scan issue to check + * @return true if an active ignore entry exists for this issue's file + */ + public boolean isIgnored(ScanIssue issue) { + if (issue == null) { + return false; + } + if (issue.getScanEngine() == com.checkmarx.eclipse.devassist.model.ScanEngine.ASCA) { + // ASCA's ignore key intentionally omits the line number (see + // createJsonKeyForIgnoreEntry) so one key can cover several distinct occurrences + // of the same rule in one file, each tracked independently by problematicLine in + // that entry's FileReference list (see isAscaVulnerabilityIgnored). That + // per-occurrence filtering already runs upstream in AscaScanResultAdaptor before a + // ScanIssue ever reaches this call (from decoration/tree-filtering call sites) - + // any ASCA ScanIssue that does reach here already has only its non-ignored + // vulnerabilities. A coarse key+path match here (ignoring problematicLine) would + // incorrectly treat this issue as ignored whenever ANY occurrence of the same rule + // in this file is ignored - including ones on a completely different line. + return false; + } + String key = createJsonKeyForIgnoreEntry(issue, ""); + if (key.isEmpty()) { + return false; + } + IgnoreEntry entry = ignoreFileManager.getIgnoreData().get(key); + if (entry == null || entry.files == null) { + return false; + } + String normalizedPath = ignoreFileManager.normalizePath(issue.getFilePath()); + return entry.files.stream() + .anyMatch(ref -> ref.isActive() && normalizedPath.equals(ref.getPath())); + } + + /** + * Triggers an immediate real-time rescan for every file referenced by the + * given (just-revived) ignore entry, scoped to just those files - matching + * the JetBrains plugin's revive behavior of a per-file rescan rather than a + * full project rescan. Since the entry no longer exists (or is inactive) in + * {@code .checkmarxIgnored} by the time this runs, the scan's ignore-file + * exclusion no longer suppresses it, so the revived finding reappears. + * + * @param entry The ignore entry that was just revived + */ + private void triggerRescanForEntry(IgnoreEntry entry) { + if (entry == null || entry.getFiles() == null || project == null) { + return; + } + org.eclipse.core.runtime.IPath projectLocation = project.getLocation(); + if (projectLocation == null) { + return; + } + String basePath = projectLocation.toOSString(); + + for (IgnoreEntry.FileReference fileRef : entry.getFiles()) { + if (fileRef == null || fileRef.getPath() == null) { + continue; + } + try { + String absolutePath = Paths.get(basePath, fileRef.getPath()).toString(); + org.eclipse.core.resources.IFile file = org.eclipse.core.resources.ResourcesPlugin.getWorkspace() + .getRoot().getFileForLocation(new org.eclipse.core.runtime.Path(absolutePath)); + if (file == null || !file.exists()) { + CxLogger.warning("RTS-Ignore: Cannot trigger rescan, file not found: " + fileRef.getPath()); + continue; + } + + // Reviving only mutates .checkmarxIgnored - the source file's content and + // mtime are untouched, so DevAssistScanStateHolder's cached state hash for + // it is still identical to what was last scanned. Without clearing it here, + // ScanManager.scanFileWithOutcome() would see hasChanged()==false and skip + // the scan entirely (no scanner ever runs), so the revived finding would + // never reappear. Clearing just this file's entry forces exactly one real + // scan cycle; the hash is repopulated normally once that scan succeeds, so + // ordinary edit-based caching for this file is unaffected afterwards. + // Use file.getLocation().toOSString() (not the manually-joined path above) + // since that is the exact key RealTimeScanJob/ScanManager use to read/write + // the cache - any formatting difference here would silently no-op the clear. + DevAssistScanStateHolder stateHolder = getOrCreateStateHolder(); + if (stateHolder != null) { + stateHolder.clearFileState(file.getLocation().toOSString()); + } + + com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob scanJob = + new com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob(file, file.getName()); + scanJob.schedule(0); + CxLogger.info("RTS-Ignore: Triggered rescan for revived entry file: " + fileRef.getPath()); + } catch (Exception e) { + CxLogger.warning("RTS-Ignore: Failed to trigger rescan for file: " + fileRef.getPath() + + " - " + e.getMessage()); + } + } + } + + /** + * Triggers an immediate real-time rescan for a single file right after an ignore write, so + * the cached scan issues (and everything decorated/filtered from them) reflect the new + * ignore state right away instead of only after the next incidental rescan. Mirrors + * {@link #triggerRescanForEntry(IgnoreEntry)}'s resolution/cache-clear/schedule pattern. + * + * @param filePath absolute path of the file to rescan + */ + private void triggerRescanForFile(String filePath) { + if (filePath == null || filePath.isEmpty() || project == null) { + return; + } + try { + org.eclipse.core.resources.IFile file = org.eclipse.core.resources.ResourcesPlugin.getWorkspace() + .getRoot().getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); + if (file == null || !file.exists()) { + CxLogger.warning("RTS-Ignore: Cannot trigger rescan, file not found: " + filePath); + return; + } + + // See triggerRescanForEntry for why the cached state hash must be cleared: ignoring + // only mutates .checkmarxIgnored, not the source file, so without this the scan would + // be skipped entirely as "unchanged". + DevAssistScanStateHolder stateHolder = getOrCreateStateHolder(); + if (stateHolder != null) { + stateHolder.clearFileState(file.getLocation().toOSString()); + } + + com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob scanJob = + new com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob(file, file.getName()); + scanJob.schedule(0); + CxLogger.info("RTS-Ignore: Triggered rescan for ignored entry's file: " + filePath); + } catch (Exception e) { + CxLogger.warning("RTS-Ignore: Failed to trigger rescan for file: " + filePath + " - " + e.getMessage()); + } + } + + /** + * Fetches (or lazily creates) the same per-project {@link DevAssistScanStateHolder} + * instance that {@code RealTimeScanJob}/{@code ScanManager} use for edit-based + * caching, via the identical session-property key. Sharing the exact instance + * (rather than constructing a disconnected one) is required for + * {@link DevAssistScanStateHolder#clearFileState(String)} to have any effect on + * the subsequent scan cycle triggered by revive. + */ + private DevAssistScanStateHolder getOrCreateStateHolder() { + if (project == null) { + return null; + } + try { + org.eclipse.core.runtime.QualifiedName stateHolderKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "state-holder"); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(stateHolderKey); + if (stateHolder == null) { + stateHolder = new DevAssistScanStateHolder(); + project.setSessionProperty(stateHolderKey, stateHolder); + } + return stateHolder; + } catch (Exception e) { + CxLogger.warning("RTS-Ignore: Failed to access scan state holder: " + e.getMessage()); + return null; + } + } + + private IgnoreEntry buildIgnoreEntry(ScanIssue issue, String clickId) { + IgnoreEntry entry = new IgnoreEntry(); + if (issue == null) { + return entry; + } + // Convert model.ScanEngine to utils.ScanEngine + com.checkmarx.eclipse.devassist.model.ScanEngine modelEngine = issue.getScanEngine(); + com.checkmarx.eclipse.devassist.utils.ScanEngine engine = null; + if (modelEngine != null) { + engine = com.checkmarx.eclipse.devassist.utils.ScanEngine.valueOf(modelEngine.toString()); + entry.type = engine; + } + + // ASCA/IAC can group several vulnerabilities under one ScanIssue (same line). The + // ScanIssue's own title/ruleId/similarityId are the group's aggregate values (e.g. + // "3 Checkmarx One Assist issues") and are identical for every vulnerability in the + // group and across every line flagged by the same rule - keying/describing the ignore + // entry off them would ignore the whole group/rule instead of just the clicked + // occurrence. Resolve the specific Vulnerability the user clicked instead. + Vulnerability vulnerability = (engine == com.checkmarx.eclipse.devassist.utils.ScanEngine.ASCA + || engine == com.checkmarx.eclipse.devassist.utils.ScanEngine.IAC) + ? resolveVulnerability(issue, clickId) : null; + + String problematicLine = ""; + if (vulnerability != null) { + entry.title = vulnerability.getTitle(); + entry.severity = vulnerability.getSeverity(); + entry.description = vulnerability.getDescription(); + entry.ruleId = vulnerability.getRuleId(); + entry.similarityId = vulnerability.getSimilarityId(); + entry.packageName = vulnerability.getTitle(); + problematicLine = vulnerability.getProblematicLine() != null ? vulnerability.getProblematicLine() : ""; + } else { + entry.title = issue.getTitle(); + entry.severity = issue.getSeverity(); + entry.description = issue.getDescription(); + entry.ruleId = issue.getRuleId(); + entry.similarityId = issue.getSimilarityId(); + if (engine == com.checkmarx.eclipse.devassist.utils.ScanEngine.CONTAINERS) { + entry.packageName = issue.getTitle() + ":" + issue.getImageTag(); + entry.imageName = issue.getTitle(); + entry.imageTag = issue.getImageTag(); + } else { + entry.packageName = issue.getTitle(); + } + } + entry.packageManager = issue.getPackageManager(); + entry.packageVersion = issue.getPackageVersion(); + entry.secretValue = issue.getSecretValue(); + entry.dateAdded = java.time.Instant.now().toString(); + + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + IgnoreEntry.FileReference ref = new IgnoreEntry.FileReference( + ignoreFileManager.normalizePath(issue.getFilePath()), + true, + issue.getLocations().get(0).getLine(), + problematicLine + ); + entry.files.add(ref); + } + return entry; + } + + /** + * Resolves the specific {@link Vulnerability} the user acted on within a (possibly + * multi-vulnerability) ScanIssue. {@code clickId} is the vulnerability id carried on the + * hover's per-vulnerability "Ignore this" link; when it's absent/empty/the quick-fix + * sentinel (e.g. "Ignore This Finding" from the Findings tree's right-click menu, which + * acts on the whole ScanIssue node rather than a specific vulnerability), falls back to + * the issue's own id - which, by construction in the ASCA/IAC adaptors, is assigned to the + * first (highest-severity) vulnerability of the *original* group. + *

    + * That id can go stale: once that original first vulnerability is itself ignored on an + * earlier pass, the adaptor drops it from {@code issue.getVulnerabilities()} on the next + * rebuild, so the id no longer matches anything in the (now-filtered) list even though + * other vulnerabilities from the same group remain. In that case, fall back to the first + * vulnerability actually present - unambiguous when it's the only one left, and otherwise + * consistent with the original "first in the group" intent of {@code getScanIssueId()}. + */ + private Vulnerability resolveVulnerability(ScanIssue issue, String clickId) { + String vulnerabilityId = (clickId == null || clickId.isEmpty() || clickId.equals(QUICK_FIX)) + ? issue.getScanIssueId() : clickId; + Vulnerability vulnerability = DevAssistUtils.getVulnerabilityDetails(issue, vulnerabilityId); + if (vulnerability == null && issue.getVulnerabilities() != null && !issue.getVulnerabilities().isEmpty()) { + vulnerability = issue.getVulnerabilities().get(0); + } + return vulnerability; + } + + /** + * Builds a unique key identifying the given scan issue's vulnerability, matching the + * composite key format used by the JetBrains plugin so ignore/revive/isIgnored checks + * are consistent across scan engines (OSS, CONTAINERS, SECRETS, IAC, ASCA). + */ + public String createJsonKeyForIgnoreEntry(ScanIssue issue, String clickId) { + if (issue == null || issue.getScanEngine() == null) return ""; + String relativePath = ignoreFileManager.normalizePath(issue.getFilePath()); + com.checkmarx.eclipse.devassist.utils.ScanEngine engine = + com.checkmarx.eclipse.devassist.utils.ScanEngine.valueOf(issue.getScanEngine().toString()); + switch (engine) { + case OSS: + return formatJsonKeyForIgnoreEntry(engine, issue.getPackageManager(), issue.getTitle(), issue.getPackageVersion()); + case CONTAINERS: + return formatJsonKeyForIgnoreEntry(engine, issue.getTitle(), issue.getImageTag(), ""); + case SECRETS: + return formatJsonKeyForIgnoreEntry(engine, issue.getTitle(), issue.getSecretValue(), relativePath); + case IAC: { + Vulnerability vulnerability = resolveVulnerability(issue, clickId); + return vulnerability != null && vulnerability.getSimilarityId() != null ? + formatJsonKeyForIgnoreEntry(engine, vulnerability.getTitle(), vulnerability.getSimilarityId(), relativePath) : ""; + } + case ASCA: { + Vulnerability vulnerability = resolveVulnerability(issue, clickId); + return vulnerability != null && vulnerability.getRuleId() != null ? + formatJsonKeyForIgnoreEntry(engine, vulnerability.getTitle(), String.valueOf(vulnerability.getRuleId()), relativePath) : ""; + } + default: + return formatJsonKeyForIgnoreEntry(engine, "", "", issue.getTitle()); + } + } + + /** + * Checks whether a specific ASCA vulnerability is ignored, based on its rule name and the + * actual source text of the line it was flagged on ("problematic line"), rather than just + * line number (which drifts as the file is edited) or file path alone (which would match + * every occurrence of the same rule anywhere in the file). Used to filter individual + * vulnerabilities out of a ScanIssue that may group several onto the same line, since ASCA's + * CLI has no ignore-file exclusion of its own (see AscaScannerService#getIgnoreFilePath) - + * this app-level check is the only enforcement point. + * + * @param vulnerability the specific vulnerability to check + * @param ignoreEntries the current ignore entries to check against + * @param filePath the file path of the issue + * @return {@code true} if this specific vulnerability is ignored; {@code false} otherwise + */ + public boolean isAscaVulnerabilityIgnored(Vulnerability vulnerability, List ignoreEntries, String filePath) { + if (vulnerability == null || ignoreEntries == null) { + return false; + } + String normalizedPath = ignoreFileManager.normalizePath(filePath); + String issueProblematicLine = vulnerability.getProblematicLine(); + String vulnTitle = vulnerability.getTitle(); + for (IgnoreEntry entry : ignoreEntries) { + if (entry.getType() != com.checkmarx.eclipse.devassist.utils.ScanEngine.ASCA) { + continue; + } + // Match by rule name: the ignore entry's packageName must match the vulnerability's title (rule name) + boolean ruleNameMatch = (entry.getPackageName() != null && entry.getPackageName().equals(vulnTitle)) + || (entry.getPackageName() == null && vulnTitle == null); + if (!ruleNameMatch || entry.getFiles() == null) { + continue; + } + for (IgnoreEntry.FileReference ref : entry.getFiles()) { + boolean pathMatch = ref.isActive() && normalizedPath.equals(ref.getPath()); + boolean problematicLineMatch = (issueProblematicLine == null && ref.getProblematicLine() == null) + || (issueProblematicLine != null && issueProblematicLine.equals(ref.getProblematicLine())); + if (pathMatch && problematicLineMatch) { + return true; + } + } + } + return false; + } + + /** + * Checks if there are any ignored entries for the specified scan engine type. + * Used to skip the (expensive) full re-scan / line-number reconciliation + * done by {@link #updateLineNumbersForIgnoredEntries} when a file's scan + * engine has no ignored entries at all. + * + * @param scanEngine The scan engine type to check for ignored entries + * @return {@code true} if there are any ignored entries for the specified scan engine, + * {@code false} otherwise + */ + public boolean hasIgnoredEntries(com.checkmarx.eclipse.devassist.utils.ScanEngine scanEngine) { + return ignoreFileManager.getIgnoreData().values().stream() + .anyMatch(entry -> entry.getType() == scanEngine); + } + + /** + * Creates a list of ignore entry keys for a given scan issue. + * For IAC and ASCA scan engines, it generates keys for each vulnerability found in the scan issue. + * For other scan engines (OSS, SECRETS, CONTAINERS), it generates a single key using the quick fix ID. + * + * @param scanIssue The scan issue to create ignore keys for + * @return A list of unique keys that can be used to identify ignore entries for this scan issue + */ + private List createIgnoreKeysForScanIssue(ScanIssue scanIssue) { + List keys = new ArrayList<>(); + if (scanIssue.getScanEngine() == com.checkmarx.eclipse.devassist.model.ScanEngine.IAC + || scanIssue.getScanEngine() == com.checkmarx.eclipse.devassist.model.ScanEngine.ASCA) { + // IAC / ASCA - build key for EACH vulnerability + if (scanIssue.getVulnerabilities() == null || scanIssue.getVulnerabilities().isEmpty()) { + return keys; + } + for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) { + String vulnerabilityId = vulnerability.getVulnerabilityId(); + if (vulnerabilityId == null || vulnerabilityId.isEmpty()) { + continue; + } + String key = createJsonKeyForIgnoreEntry(scanIssue, vulnerabilityId); + if (!key.isEmpty()) { + keys.add(key); + } + } + } else { + String key = createJsonKeyForIgnoreEntry(scanIssue, QUICK_FIX); + if (!key.isEmpty()) { + keys.add(key); + } + } + return keys; + } + + /** + * Updates line numbers for ignored entries (OSS/SECRETS/CONTAINERS/IAC) based on a fresh, + * UNFILTERED scan of the file (i.e. run without passing the ignore file to the scan engine, + * so already-ignored issues are still present in the results). If a user edits a file above an + * ignored finding, the finding's line shifts - without this reconciliation the gutter + * icon/marker for that ignored finding would render at its stale line. Also removes ignore + * entries whose finding is no longer present in the scan results (e.g. the vulnerable code was + * deleted). + * + * @param fullScanResults The UNFILTERED scan results for the file + * @param filePath The path of the file that was scanned and needs line number updates + */ + public void updateLineNumbersForIgnoredEntries(ScanResult fullScanResults, String filePath) { + List allIssuesForFile = fullScanResults.getIssues(); + if (allIssuesForFile == null || allIssuesForFile.isEmpty()) { + CxLogger.info(String.format("RTS-Ignore: No issues found in scan results for file: %s", filePath)); + return; + } + com.checkmarx.eclipse.devassist.model.ScanEngine modelScanEngine = allIssuesForFile.get(0).getScanEngine(); + if (modelScanEngine == null) { + CxLogger.info(String.format("RTS-Ignore: Scan engine type is null for file: %s", filePath)); + return; + } + com.checkmarx.eclipse.devassist.utils.ScanEngine scanEngineType = + com.checkmarx.eclipse.devassist.utils.ScanEngine.valueOf(modelScanEngine.toString()); + + boolean hasChanges = false; + List keysToRemove = new ArrayList<>(); + Map scanIssueKeyMap = new HashMap<>(); + for (ScanIssue scanIssue : allIssuesForFile) { + for (String key : createIgnoreKeysForScanIssue(scanIssue)) { + scanIssueKeyMap.put(key, scanIssue); + } + } + String relativePath = ignoreFileManager.normalizePath(filePath); + for (Map.Entry mapEntry : ignoreFileManager.getIgnoreData().entrySet()) { + IgnoreEntry ignoreEntry = mapEntry.getValue(); + if (ignoreEntry.getType() != scanEngineType) { + continue; // Skip entries from different scan engines + } + ScanIssue matchingScanIssue = scanIssueKeyMap.get(mapEntry.getKey()); + if (matchingScanIssue != null) { + String matchingIssuePath = ignoreFileManager.normalizePath(matchingScanIssue.getFilePath()); + if (matchingIssuePath.equals(relativePath)) { + if (matchingScanIssue.getLocations() == null || matchingScanIssue.getLocations().isEmpty()) { + continue; + } + int newLineNumber = matchingScanIssue.getLocations().get(0).getLine(); + for (IgnoreEntry.FileReference fileRef : ignoreEntry.getFiles()) { + if (fileRef.getPath().equals(relativePath) && fileRef.isActive()) { + Integer oldLineNumber = fileRef.getLine(); + if (oldLineNumber == null || oldLineNumber != newLineNumber) { + fileRef.setLine(newLineNumber); + hasChanges = true; + } + } + } + } + } else { + // Not found in THIS file's reconciliation scan - the package is gone from this file + // only. Remove just this file's reference(s), not the whole entry, so an ignore + // recorded separately for another file (e.g. "ignore this" clicked in both a root + // and a nested manifest with the same dependency) survives intact. Only drop the + // entry entirely once it has no file references left at all. + boolean hasFileRefForCurrentFile = ignoreEntry.getFiles().stream() + .anyMatch(fileRef -> fileRef.getPath().equals(relativePath) && fileRef.isActive()); + if (hasFileRefForCurrentFile) { + ignoreEntry.getFiles().removeIf(fileRef -> fileRef.getPath().equals(relativePath)); + hasChanges = true; + if (ignoreEntry.getFiles().isEmpty()) { + keysToRemove.add(mapEntry.getKey()); + } + } + } + } + updateInIgnoredEntries(keysToRemove, hasChanges, relativePath); + } + + /** + * Removes entries marked for removal and persists any line-number changes to disk. + * + * @param keysToRemove List of keys to remove from ignore data + * @param toUpdate Flag indicating whether line numbers were updated and need to be saved + * @param relativePath Relative path of the file being processed + */ + private void updateInIgnoredEntries(List keysToRemove, boolean toUpdate, String relativePath) { + if (!keysToRemove.isEmpty()) { + for (String keyToRemove : keysToRemove) { + ignoreFileManager.getIgnoreData().remove(keyToRemove); + toUpdate = true; + } + } + if (toUpdate) { + ignoreFileManager.saveIgnoreDataToDisk(); + CxLogger.info(String.format("RTS-Ignore: Line numbers updated and saved for file: %s", relativePath)); + } else { + CxLogger.info(String.format("RTS-Ignore: No line number changes detected for file: %s", relativePath)); + } + } + + /** + * Updates line numbers for ignored ASCA entries based on new scan results, using + * problematicLine content for matching (ASCA's ignore key intentionally omits the line + * number - see {@link #createJsonKeyForIgnoreEntry} - so occurrences of the same rule in a + * file are tracked by the source text of the flagged line rather than its position, since + * that drifts as the file is edited). Removes file references/entries whose problematicLine + * is no longer present in the scan result (e.g. the vulnerable code was deleted). + *

    + * The passed-in {@code fullScanResults} must be UNFILTERED (built with an + * {@link com.checkmarx.eclipse.devassist.scanners.asca.AscaScanResultAdaptor} that keeps + * already-ignored vulnerabilities) so every currently-ignored occurrence can still be matched. + * + * @param fullScanResults The UNFILTERED scan results containing updated line numbers and issues + * @param filePath The path of the file that was scanned and needs line number updates + */ + public void updateLineNumbersForIgnoredEntriesByProblematicLine(ScanResult fullScanResults, String filePath) { + List allIssuesForFile = fullScanResults.getIssues(); + if (allIssuesForFile == null || allIssuesForFile.isEmpty()) { + CxLogger.info(String.format("ASCA-Ignore: No issues found in scan results for file: %s", filePath)); + return; + } + String relativePath = ignoreFileManager.normalizePath(filePath); + boolean hasChanges = false; + + List vulnerabilitiesWithLine = new ArrayList<>(); + Set presentProblematicLines = new HashSet<>(); + for (ScanIssue scanIssue : allIssuesForFile) { + if (scanIssue.getVulnerabilities() != null) { + for (Vulnerability v : scanIssue.getVulnerabilities()) { + int line = (scanIssue.getLocations() != null && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() : 0; + vulnerabilitiesWithLine.add(new VulnerabilityWithLine(v.getProblematicLine(), line)); + if (v.getProblematicLine() != null) { + presentProblematicLines.add(v.getProblematicLine()); + } + } + } + } + + List keysToRemove = new ArrayList<>(); + for (Map.Entry mapEntry : ignoreFileManager.getIgnoreData().entrySet()) { + IgnoreEntry ignoreEntry = mapEntry.getValue(); + if (ignoreEntry.getType() != com.checkmarx.eclipse.devassist.utils.ScanEngine.ASCA) { + continue; // Only process ASCA entries + } + List fileRefs = ignoreEntry.getFiles(); + List fileRefsToRemove = new ArrayList<>(); + for (IgnoreEntry.FileReference fileRef : fileRefs) { + if (fileRef.getPath().equals(relativePath) && fileRef.isActive()) { + String ignoredProblematicLine = fileRef.getProblematicLine(); + VulnerabilityWithLine match = vulnerabilitiesWithLine.stream() + .filter(vwl -> Objects.equals(vwl.problematicLine, ignoredProblematicLine)) + .findFirst().orElse(null); + if (match != null && match.line > 0 + && (fileRef.getLine() == null || fileRef.getLine() != match.line)) { + fileRef.setLine(match.line); + hasChanges = true; + } + if (ignoredProblematicLine == null || !presentProblematicLines.contains(ignoredProblematicLine)) { + fileRefsToRemove.add(fileRef); + hasChanges = true; + } + } + } + if (!fileRefsToRemove.isEmpty()) { + fileRefs.removeAll(fileRefsToRemove); + } + if (ignoreEntry.getFiles().isEmpty()) { + keysToRemove.add(mapEntry.getKey()); + } + } + for (String keyToRemove : keysToRemove) { + ignoreFileManager.getIgnoreData().remove(keyToRemove); + hasChanges = true; + } + if (hasChanges) { + ignoreFileManager.saveIgnoreDataToDisk(); + CxLogger.info(String.format( + "ASCA-Ignore: Line numbers and obsolete entries updated by problematicLine and saved for file: %s", + relativePath)); + } else { + CxLogger.info(String.format( + "ASCA-Ignore: No line number or entry changes detected by problematicLine for file: %s", + relativePath)); + } + } + + /** Helper for matching a problematicLine to its current line number. */ + private static class VulnerabilityWithLine { + final String problematicLine; + final int line; + + VulnerabilityWithLine(String problematicLine, int line) { + this.problematicLine = problematicLine; + this.line = line; + } + } + + /** + * Removes all ASCA ignore entries/file references for a file when a full re-scan finds no + * issues at all in it (e.g. every flagged line was deleted). + * + * @param filePath The path of the file for which ignore entries should be removed + */ + public void removeIgnoreEntriesForFileIfEmpty(String filePath) { + String relativePath = ignoreFileManager.normalizePath(filePath); + List keysToRemove = new ArrayList<>(); + boolean removed = false; + for (Map.Entry mapEntry : ignoreFileManager.getIgnoreData().entrySet()) { + IgnoreEntry ignoreEntry = mapEntry.getValue(); + if (ignoreEntry.getType() != com.checkmarx.eclipse.devassist.utils.ScanEngine.ASCA) { + continue; + } + List fileRefs = ignoreEntry.getFiles(); + fileRefs.removeIf(fileRef -> fileRef.getPath().equals(relativePath)); + if (ignoreEntry.getFiles().isEmpty()) { + keysToRemove.add(mapEntry.getKey()); + } + } + for (String keyToRemove : keysToRemove) { + ignoreFileManager.getIgnoreData().remove(keyToRemove); + removed = true; + } + if (removed) { + ignoreFileManager.saveIgnoreDataToDisk(); + CxLogger.info(String.format("ASCA-Ignore: Removed ignore entries for file with no issues: %s", relativePath)); + } + } + + private String formatJsonKeyForIgnoreEntry(com.checkmarx.eclipse.devassist.utils.ScanEngine scanEngine, + String title, String secondary, String path) { + if (scanEngine == com.checkmarx.eclipse.devassist.utils.ScanEngine.CONTAINERS) { + return format("%s:%s:%s", scanEngine, title, secondary); + } else { + return format("%s:%s:%s:%s", scanEngine, title, secondary, path); + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/TempItem.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/TempItem.java new file mode 100644 index 00000000..abe38ab7 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ignore/TempItem.java @@ -0,0 +1,173 @@ +package com.checkmarx.eclipse.devassist.ignore; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class TempItem { + private String Title; + private String SecretValue; + private String SimilarityID; + private String FileName; + private Integer Line; + private Integer RuleID; + private String PackageManager; + private String PackageName; + private String PackageVersion; + private String ImageName; + private String ImageTag; + + public TempItem() { + } + + + public static TempItem forOss(String pm, String name, String version) { + TempItem t = new TempItem(); + t.PackageManager = pm; + t.PackageName = name; + t.PackageVersion = version; + return t; + } + + public static TempItem forSecret(String title, String secretValue) { + TempItem t = new TempItem(); + t.Title = title; + t.SecretValue = secretValue; + return t; + } + + public static TempItem forIac(String title, String similarityId) { + TempItem t = new TempItem(); + t.Title = title; + t.SimilarityID = similarityId; + return t; + } + + public static TempItem forContainer(String imageName, String imageTag) { + TempItem t = new TempItem(); + t.ImageName = imageName; + t.ImageTag = imageTag; + return t; + } + + public static TempItem forAsca(String fileName, Integer line, Integer ruleId) { + TempItem t = new TempItem(); + t.FileName = fileName; + t.Line = line; + t.RuleID = ruleId; + return t; + } + + + public String getTitle() { + return Title; + } + + + public void setTitle(String title) { + Title = title; + } + + + public String getSecretValue() { + return SecretValue; + } + + + public void setSecretValue(String secretValue) { + SecretValue = secretValue; + } + + + public String getSimilarityID() { + return SimilarityID; + } + + + public void setSimilarityID(String similarityID) { + SimilarityID = similarityID; + } + + + public String getFileName() { + return FileName; + } + + + public void setFileName(String fileName) { + FileName = fileName; + } + + + public Integer getLine() { + return Line; + } + + + public void setLine(Integer line) { + Line = line; + } + + + public Integer getRuleID() { + return RuleID; + } + + + public void setRuleID(Integer ruleID) { + RuleID = ruleID; + } + + + public String getPackageManager() { + return PackageManager; + } + + + public void setPackageManager(String packageManager) { + PackageManager = packageManager; + } + + + public String getPackageName() { + return PackageName; + } + + + public void setPackageName(String packageName) { + PackageName = packageName; + } + + + public String getPackageVersion() { + return PackageVersion; + } + + + public void setPackageVersion(String packageVersion) { + PackageVersion = packageVersion; + } + + + public String getImageName() { + return ImageName; + } + + + public void setImageName(String imageName) { + ImageName = imageName; + } + + + public String getImageTag() { + return ImageTag; + } + + + public void setImageTag(String imageTag) { + ImageTag = imageTag; + } + + +} 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..3949fa26 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java @@ -0,0 +1,369 @@ +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. + // ✅ Only fall back to a fresh scan when the caller genuinely has no + // issue list yet (null). Callers such as ResultPublisher always pass a + // real (possibly empty) list here after a completed scan cycle - an + // empty list is a legitimate "file has zero issues" result, not a + // signal to re-scan. Treating isEmpty() as "need a fresh scan" caused a + // redundant, wasted re-scan on every zero-issue publish (harmless + // since ScanManager's state-hash dedup made it a no-op, but noisy and + // misleading in logs). + List allScanIssues = problemHelper.getScanIssueList(); + if (allScanIssues == null) { + 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()); + // Clear stale cached problem descriptors for this file - otherwise a + // later getExistingProblems() lookup could resurrect descriptors for + // issues that no longer exist. + problemHelper.getProblemHolderService().removeProblemDescriptorsForFile(problemHelper.getFilePath()); + 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(); + + // NOTE: do NOT call problemHolderService.addScanIssues() here. The + // only caller of this method (ResultPublisher.createAndRenderDecorations) + // already cached this exact issue list via ResultPublisher.updateFindingsView() + // moments earlier. Re-caching the same list here re-publishes + // ISSUES_UPDATED_TOPIC a second time for the same scan cycle, causing + // CxFindingsView to run a second, redundant applyDecorationsToOpenEditors() + // pass concurrently with the direct decorateUI() call below - widening the + // race window with IgnoreFileManager's async file-watcher callback and + // producing inconsistent ignored/severity icon states on the same line. + + // 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()); + + // Build descriptors WITHOUT per-issue decoration: decorateUI() below + // already redraws the full, merged issue list in one pass. Passing + // isDecoratorEnabled=true here used to make ScanIssueProcessor call + // ProblemDecorator.highlightLineAddGutterIconForProblem() once per + // issue, and each of those calls clears and rebuilds ALL annotations + // for the file (ProblemDecorator.decorateEditor() unconditionally + // clears before adding) - so a 4-issue file flickered through 4 + // single-issue annotation states before the final full redraw, + // occasionally leaving the hover to sample an incomplete annotation + // model mid-flicker. + List descriptors = createProblemDescriptors( + problemHelper, false); + + // 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 or when NO issues found. + * + * ✅ CRITICAL: This is called when scan returns 0 issues. + * We MUST clear old annotations/decorations from the editor. + * By calling decorateEditor() with the (empty) list, it will: + * 1. Clear old annotations via clearAnnotations() + * 2. Return early since the list is empty (nothing to add) + * + * @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() + + " with " + scanIssueList.size() + " issues"); + // Clear decorations from editor (this also works with empty list) + ProblemDecorator.decorateEditor(file, scanIssueList); + } 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..2e3f4b22 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java @@ -0,0 +1,241 @@ +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 org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; + +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<>(); + + /** + * Listener that removes completed jobs from pendingScans. + * Prevents unbounded memory growth from accumulated job entries. + * The done() callback fires for both successful completion and cancellation. + */ + private final IJobChangeListener jobCompletionListener = new IJobChangeListener() { + @Override + public void done(IJobChangeEvent event) { + removeCompletedJob(event.getJob()); + } + + @Override + public void aboutToRun(IJobChangeEvent event) { + } + + @Override + public void awake(IJobChangeEvent event) { + } + + @Override + public void running(IJobChangeEvent event) { + } + + @Override + public void scheduled(IJobChangeEvent event) { + } + + @Override + public void sleeping(IJobChangeEvent event) { + } + }; + + /** + * Remove a completed job from pendingScans map. + * Called when job completes or is cancelled. + */ + private void removeCompletedJob(org.eclipse.core.runtime.jobs.Job job) { + // Find and remove the job from pendingScans by matching the job object + for (Map.Entry entry : pendingScans.entrySet()) { + if (entry.getValue() == job) { + pendingScans.remove(entry.getKey()); + CxLogger.info(LOG_TAG + " Removed completed scan from pending: " + entry.getKey()); + return; + } + } + } + + /** + * 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()); + + // Attach listener to clean up entry when job completes or is cancelled + scanJob.addJobChangeListener(jobCompletionListener); + + // 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..adf6a684 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -0,0 +1,982 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.DefaultPositionUpdater; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IPositionUpdater; +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.ignore.IgnoreEntry; +import com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +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<>(); + + // Live-tracked line positions for ignored entries, keyed by + // "##<originalIgnoreFileLine>". Registered on the + // document via Position/PositionUpdater so edits made above an ignored line + // shift it automatically (Eclipse's equivalent of JetBrains' RangeMarker), + // instead of re-reading the static line number stored in .checkmarxIgnored + // on every redecoration pass. + private static final String IGNORED_POSITION_CATEGORY = "com.checkmarx.eclipse.ignoredLineTracking"; + private static final Map<String, TrackedPosition> ignoredLinePositions = new HashMap<>(); + + private static final class TrackedPosition { + final IDocument document; + final Position position; + + TrackedPosition(IDocument document, Position position) { + this.document = document; + this.position = position; + } + } + + /** + * Returns the current (edit-tracked) offset for an ignored entry's line, + * creating and registering a tracked {@link Position} the first time this + * entry is seen for the given document. Falls back to the given original + * line's offset if tracking can't be set up. + */ + private static int resolveTrackedIgnoredOffset(IDocument document, String key, int originalLineOffset) { + TrackedPosition tracked = ignoredLinePositions.get(key); + if (tracked != null && tracked.document == document && !tracked.position.isDeleted()) { + return Math.max(0, Math.min(tracked.position.getOffset(), document.getLength())); + } + + try { + if (!document.containsPositionCategory(IGNORED_POSITION_CATEGORY)) { + document.addPositionCategory(IGNORED_POSITION_CATEGORY); + IPositionUpdater updater = new DefaultPositionUpdater(IGNORED_POSITION_CATEGORY); + document.addPositionUpdater(updater); + } + Position position = new Position(originalLineOffset, 0); + document.addPosition(IGNORED_POSITION_CATEGORY, position); + ignoredLinePositions.put(key, new TrackedPosition(document, position)); + return originalLineOffset; + } catch (BadLocationException | BadPositionCategoryException e) { + return originalLineOffset; + } + } + + /** + * Normalize file path for consistent key lookups in fileAnnotations map. + * Converts to lowercase on Windows and uses forward slashes. + * This prevents path key mismatches between different code paths that might + * format paths differently (e.g. Maven editor vs regular editor). + */ + private static String normalizeFilePath(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return ""; + } + String normalized = filePath.replace("\\", "/"); + // On Windows, normalize to lowercase for case-insensitive matching + if (System.getProperty("os.name").toLowerCase().contains("win")) { + normalized = normalized.toLowerCase(); + } + CxLogger.info(LOG_TAG + "[PATH_NORMALIZE] Original: " + filePath + " -> Normalized: " + normalized); + return normalized; + } + + /** + * 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<ScanIssue> 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(); + String normalizedFilePath = normalizeFilePath(filePath); + + CxLogger.info(LOG_TAG + "============================================"); + CxLogger.info(LOG_TAG + "[DECORATE_START] File: " + filePath); + CxLogger.info(LOG_TAG + "[DECORATE_START] Normalized: " + normalizedFilePath); + CxLogger.info(LOG_TAG + "[DECORATE_START] ScanIssues count: " + scanIssues.size()); + CxLogger.info(LOG_TAG + "[DECORATE_START] Current fileAnnotations keys: " + fileAnnotations.keySet()); + + try { + // Find open editor for this file + ITextEditor editor = findOpenEditor(file); + if (editor == null) { + CxLogger.info(LOG_TAG + "[DECORATE_SKIP] 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 + "[DECORATE_FAIL] 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 + CxLogger.info(LOG_TAG + "[CLEAR_START] Clearing previous annotations for: " + normalizedFilePath); + clearAnnotations(normalizedFilePath, annotationModel); + + // Remove previous IMarkers for this file too - ensureMarker() below only adds + // markers, so without this, issues that were ignored/resolved/filtered out + // since the last decoration (e.g. via the ignore action) leave a stale gutter + // icon and Problems-view entry behind even though the squiggly is gone. + MarkerIssueMapper.clearAllMarkers(file); + + // Add new annotations for each issue. Note: we do NOT early-return when + // scanIssues is empty - a file can go from "has active findings" to "fully + // ignored" (all findings ignored one-by-one), and we still need to run the + // ignored-entries pass below so the "ignored" gutter icon replaces the + // severity icon that was just cleared above. + List<Annotation> annotations = new ArrayList<>(); + Set<Integer> activeLines = new HashSet<>(); + + IProject project = file.getProject(); + IgnoreManager ignoreManager = project != null ? IgnoreManager.getInstance(project) : null; + + for (ScanIssue issue : scanIssues) { + try { + if (ignoreManager != null && ignoreManager.isIgnored(issue)) { + continue; + } + // Ensure the IMarker CheckmarxMarkerResolutionGenerator's + // Ctrl+1/quick-fix-in-hover + // actions anchor to exists as soon as the squiggly does, rather than only after + // the + // user separately navigates to this finding from the Findings view. + MarkerIssueMapper.ensureMarker(file, issue); + + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + activeLines.add(issue.getLocations().get(0).getLine()); + } + + 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(); + } + } + + // Draw the theme-aware "ignored" gutter icon for every active + // .checkmarxIgnored entry on this file whose line isn't already owned by an + // active (non-ignored) finding above. + annotations.addAll(decorateIgnoredEntries(file, editor, annotationModel, activeLines)); + + // Store annotations for later cleanup (using normalized path) + fileAnnotations.put(normalizedFilePath, annotations); + + CxLogger.info(LOG_TAG + "[DECORATE_COMPLETE] Added " + annotations.size() + + " annotations to editor"); + CxLogger.info(LOG_TAG + "[DECORATE_COMPLETE] FileAnnotations now has: " + fileAnnotations.size() + " entries"); + CxLogger.info(LOG_TAG + "============================================"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + "[DECORATE_ERROR] Error decorating editor: " + + e.getMessage()); + e.printStackTrace(); + CxLogger.info(LOG_TAG + "============================================"); + } + } + + /** + * 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(), + issue); + return annotation; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating annotation: " + + e.getMessage()); + return null; + } + } + + /** + * Draw a theme-aware "ignored" gutter annotation for every active + * {@code .checkmarxIgnored} entry that targets this file, skipping any line + * that an active (non-ignored) finding already owns in this decoration pass. + * + * Uses a zero-length {@link Position} so only the vertical ruler/gutter icon + * is rendered - no in-text squiggle - matching the requirement that an + * ignored finding shows solely the "ignored" icon in place of its previous + * severity icon. + * + * @param file File being decorated + * @param editor Open text editor for the file + * @param annotationModel Annotation model to add the ignored annotations to + * @param activeLines 1-based line numbers already covered by an active + * (non-ignored) finding in this same pass + * @return The list of ignored annotations added, for later cleanup + */ + private static List<Annotation> decorateIgnoredEntries(IFile file, ITextEditor editor, + IAnnotationModel annotationModel, Set<Integer> activeLines) { + List<Annotation> result = new ArrayList<>(); + try { + IProject project = file.getProject(); + if (project == null) { + return result; + } + + IgnoreFileManager ignoreFileManager = IgnoreFileManager.getInstance(project); + String normalizedPath = ignoreFileManager.normalizePath(file.getLocation().toOSString()); + + IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + if (document == null) { + return result; + } + + String annotationType = "com.checkmarx.eclipse.findings.ignored" + + (DevAssistUtils.isDarkTheme() ? "_dark" : ""); + + for (IgnoreEntry entry : ignoreFileManager.getAllIgnoreEntries()) { + if (entry.getFiles() == null) { + continue; + } + for (IgnoreEntry.FileReference ref : entry.getFiles()) { + if (!ref.isActive() || ref.getLine() == null) { + continue; + } + if (!normalizedPath.equals(ref.getPath())) { + continue; + } + + int zeroBasedLine = ref.getLine() - 1; + if (zeroBasedLine < 0 || zeroBasedLine >= document.getNumberOfLines()) { + continue; + } + + try { + IRegion lineInfo = document.getLineInformation(zeroBasedLine); + String trackingKey = normalizedPath + "#" + entry.getTitle() + "#" + ref.getLine(); + int trackedOffset = resolveTrackedIgnoredOffset(document, trackingKey, lineInfo.getOffset()); + int currentLine = document.getLineOfOffset(trackedOffset) + 1; + + if (activeLines.contains(currentLine)) { + // An active (non-ignored) finding already owns this line this pass. + continue; + } + + Position pos = new Position(trackedOffset, 0); + + String description = entry.getDescription() != null ? entry.getDescription() + : entry.getPackageName(); + FindingsAnnotation annotation = new FindingsAnnotation( + annotationType, + entry.getTitle(), + "Ignored: " + description); + annotationModel.addAnnotation(annotation, pos); + result.add(annotation); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error adding ignored annotation: " + e.getMessage()); + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error decorating ignored entries: " + e.getMessage()); + } + return result; + } + + /** + * Map severity level to theme-aware custom Findings annotation type. + * Returns dark theme variant in dark theme, light variant in light theme. + * 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}[_dark]) + */ + private static String mapSeverityToAnnotationType(String severity) { + // Append _dark suffix if dark theme is active + String themeSuffix = DevAssistUtils.isDarkTheme() ? "_dark" : ""; + + if (severity == null) { + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; + } + String upper = severity.toUpperCase(); + if (upper.contains("MALICIOUS")) { + return "com.checkmarx.eclipse.findings.malicious" + themeSuffix; + } + if (upper.contains("CRITICAL") || upper.contains("ERROR")) { + return "com.checkmarx.eclipse.findings.critical" + themeSuffix; + } + if (upper.contains("HIGH")) { + return "com.checkmarx.eclipse.findings.high" + themeSuffix; + } + if (upper.contains("MEDIUM")) { + return "com.checkmarx.eclipse.findings.medium" + themeSuffix; + } + if (upper.contains("LOW") || upper.contains("INFO")) { + return "com.checkmarx.eclipse.findings.low" + themeSuffix; + } + if (upper.contains("UNKNOWN")) { + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; + } + if (upper.contains("OK")) { + return "com.checkmarx.eclipse.findings.ok" + themeSuffix; + } + if (upper.contains("IGNORED")) { + return "com.checkmarx.eclipse.findings.ignored" + themeSuffix; + } + + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; + } + + /** + * 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 { + String normalizedPath = normalizeFilePath(filePath); + CxLogger.info(LOG_TAG + "[CLEAR_DEBUG] Looking up with normalized path: " + normalizedPath); + CxLogger.info(LOG_TAG + "[CLEAR_DEBUG] Available keys in fileAnnotations: " + fileAnnotations.keySet()); + + List<Annotation> previousAnnotations = fileAnnotations.get(normalizedPath); + if (previousAnnotations != null) { + CxLogger.info(LOG_TAG + "[CLEAR_DEBUG] Found " + previousAnnotations.size() + " annotations to remove"); + for (Annotation annotation : previousAnnotations) { + annotationModel.removeAnnotation(annotation); + CxLogger.info(LOG_TAG + "[CLEAR_DEBUG] Removed annotation: " + annotation); + } + fileAnnotations.remove(normalizedPath); + + CxLogger.info(LOG_TAG + "[CLEAR_SUCCESS] Cleared " + previousAnnotations.size() + + " previous annotations for: " + normalizedPath); + } else { + // FALLBACK: If lookup failed, try direct removal from annotation model + CxLogger.warning(LOG_TAG + "[CLEAR_FALLBACK] No annotations found in map for: " + normalizedPath); + CxLogger.warning(LOG_TAG + "[CLEAR_FALLBACK] Attempting direct removal of FindingsAnnotations from model"); + List<Annotation> toRemove = new ArrayList<>(); + annotationModel.getAnnotationIterator().forEachRemaining(ann -> { + if (ann instanceof FindingsAnnotation) { + toRemove.add(ann); + CxLogger.info(LOG_TAG + "[CLEAR_FALLBACK] Will remove FindingsAnnotation: " + ann); + } + }); + for (Annotation ann : toRemove) { + annotationModel.removeAnnotation(ann); + } + if (!toRemove.isEmpty()) { + CxLogger.info(LOG_TAG + "[CLEAR_FALLBACK] Removed " + toRemove.size() + " FindingsAnnotations directly from model"); + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + "[CLEAR_ERROR] Error clearing annotations: " + + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Clear all annotations from all open editors (used on logout). + * Removes all FindingsAnnotation objects from the annotation models + * of currently open editors. + */ + public static void clearAllAnnotations() { + try { + CxLogger.info(LOG_TAG + "[CLEAR_ALL_START] Clearing all annotations from all open editors"); + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + CxLogger.warning(LOG_TAG + "[CLEAR_ALL] Workbench is null"); + return; + } + + int editorCount = 0; + int annotationCount = 0; + + for (var window : workbench.getWorkbenchWindows()) { + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + continue; + } + + // Get all open editors + org.eclipse.ui.IEditorReference[] editors = page.getEditorReferences(); + for (org.eclipse.ui.IEditorReference editorRef : editors) { + try { + org.eclipse.ui.IEditorPart editorPart = editorRef.getEditor(false); + if (editorPart == null) { + continue; + } + + // Use adapter pattern to get ITextEditor + ITextEditor editor = editorPart.getAdapter(ITextEditor.class); + if (editor == null) { + continue; + } + + editorCount++; + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + if (annotationModel == null) { + continue; + } + + // Remove all FindingsAnnotation objects + java.util.List<Annotation> toRemove = new java.util.ArrayList<>(); + annotationModel.getAnnotationIterator().forEachRemaining(annotation -> { + if (annotation instanceof FindingsAnnotation) { + toRemove.add(annotation); + } + }); + + for (Annotation annotation : toRemove) { + annotationModel.removeAnnotation(annotation); + annotationCount++; + } + CxLogger.info(LOG_TAG + "[CLEAR_ALL] Cleared " + toRemove.size() + " annotations from editor " + editorCount); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + "[CLEAR_ALL_ERROR] Error clearing annotations from editor: " + e.getMessage()); + } + } + } + + // Clear the fileAnnotations map + CxLogger.info(LOG_TAG + "[CLEAR_ALL] Clearing fileAnnotations map with " + fileAnnotations.size() + " entries"); + fileAnnotations.clear(); + CxLogger.info(LOG_TAG + "[CLEAR_ALL_COMPLETE] All annotations cleared - Editors: " + editorCount + ", Annotations removed: " + annotationCount); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + "[CLEAR_ALL_ERROR] Error clearing all 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<Annotation> 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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<ScannerService> supportedScanners; + private final List<ScanIssue> scanIssueList; + private final ProblemHolderService problemHolderService; + private final ProblemDecorator problemDecorator; + + /** + * Constructor for ProblemHelper. + */ + public ProblemHelper(IFile file, IProject project, String filePath, + IDocument document, List<ScannerService> supportedScanners, + List<ScanIssue> 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<ScannerService> getSupportedScanners() { + return supportedScanners; + } + + public List<ScanIssue> 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<ScannerService> supportedScanners; + private List<ScanIssue> 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<ScannerService> supportedScanners) { + this.supportedScanners = supportedScanners; + return this; + } + + public Builder scanIssueList(List<ScanIssue> 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..70579c6f --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -0,0 +1,311 @@ +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<String, List<ScanIssue>> fileToScanIssues = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, List<ProblemDescriptor>> 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"); + 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<ScanIssue> 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<ScanIssue> getScanIssuesByFile(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + + List<ScanIssue> 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<String, List<ScanIssue>> getAllScanIssues() { + + Map<String, List<ScanIssue>> result = new HashMap<>(); + for (Map.Entry<String, List<ScanIssue>> 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<ScanIssue> newIssues) { + if (filePath == null || newIssues == null) { + return; + } + + List<ScanIssue> existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + Map<String, ScanIssue> 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<ScanIssue> existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + List<ScanIssue> 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<String> removeAllIssuesForScanner(String scannerType) { + List<String> affectedFiles = new ArrayList<>(); + if (scannerType == null) { + return affectedFiles; + } + + for (Map.Entry<String, List<ScanIssue>> 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 or logout). + */ + public void clearAll() { + fileToScanIssues.clear(); + fileToProblemDescriptors.clear(); + CxLogger.info(LOG_TAG + " All caches cleared"); + publishIssuesUpdated(); + } + + /** + * 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<String, List<ScanIssue>> 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<ScanIssue> 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<ProblemDescriptor> 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<ProblemDescriptor> getProblemDescriptors(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + List<ProblemDescriptor> 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. + * <p> + * 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("<a>Open Eclipse Marketplace</a>"); + 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..aa7df068 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/CopilotIntegration.java @@ -0,0 +1,244 @@ +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). + * <p> + * Opens the Copilot Chat view in <b>Agent</b> mode with a pre-filled prompt in a + * <b>new chat session</b> and submits it automatically, using the command + * contributed by the Copilot plugin: {@code com.microsoft.copilot.eclipse.commands.openChatView}. + * Each prompt is opened in its own isolated chat session for independent Agent conversations. + * <p> + * Fallback strategy, in order: + * <ol> + * <li>If GitHub Copilot for Eclipse is not installed, a notification is shown + * inviting the user to install it from the Eclipse Marketplace.</li> + * <li>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.</li> + * </ol> + * 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"; + + /** New conversation. */ + private static final String COPILOT_NEW_CONVERSATION_COMMAND = "com.microsoft.copilot.eclipse.commands.newConversation"; + + private static final String CHAT_MODE_AGENT = "Agent"; + private static final String CHAT_MODE_ASK = "Ask"; + + 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 the specified mode with the given prompt in a new chat + * session and submits it automatically. + * <p> + * Each call creates a new chat session instead of reusing an existing + * conversation, ensuring isolated contexts for each prompt. + * <p> + * 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 + * @param chatMode the chat mode to use ("Agent" for autonomous fixes, "Ask" for explanations) + * @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, String chatMode) { + 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 (openChatInModeAndSend(prompt, chatMode)) { + CxLogger.info(LOG_PREFIX + " Prompt sent to Copilot Chat in " + chatMode + " mode and submitted automatically"); + return true; + } + + CxLogger.warning(LOG_PREFIX + " Could not invoke Copilot's open chat command - falling back to clipboard"); + return false; + } + + /** + * Opens GitHub Copilot Chat in Agent mode with the given prompt in a new chat + * session and submits it automatically. + * <p> + * Convenience method that calls sendPromptToCopilot(prompt, "Agent"). + * + * @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) { + return sendPromptToCopilot(prompt, CHAT_MODE_AGENT); + } + + /** + * 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 the specified mode, + * pre-filling the prompt, and requesting an automatic submit. + * + * @param prompt the prompt to place in the chat input + * @param chatMode the chat mode to use ("Agent" or "Ask") + * @return true if the command was found, enabled, and executed without error + */ + private static boolean openChatInModeAndSend(String prompt, String chatMode) { + try { + // 1. Run the reset command synchronously on the UI Thread + Display.getDefault().syncExec(() -> { + try { + ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); + if (commandService == null) return; + + Command newConvCommand = commandService.getCommand(COPILOT_NEW_CONVERSATION_COMMAND); + if (newConvCommand != null && newConvCommand.isDefined() && newConvCommand.isEnabled()) { + newConvCommand.executeWithChecks(new ExecutionEvent(newConvCommand, new HashMap<>(), null, null)); + } + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error clearing conversation state: " + e.getMessage()); + } + }); + + // 2. Schedule mode switch and prompt send in sequence via background thread + // This ensures mode is fully initialized before prompt is sent + Thread executionThread = new Thread(() -> { + try { + // Wait for chat UI to stabilize after reset + Thread.sleep(450); + + // Step A: Switch to the specified mode first + Display.getDefault().syncExec(() -> { + try { + ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); + if (commandService == null) return; + + Command command = commandService.getCommand(COPILOT_OPEN_CHAT_COMMAND); + if (command != null && command.isDefined() && command.isEnabled()) { + Map<String, String> modeParams = new HashMap<>(); + modeParams.put(PARAM_MODE, chatMode != null ? chatMode : CHAT_MODE_AGENT); + command.executeWithChecks(new ExecutionEvent(command, modeParams, null, null)); + } + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Mode switch failed: " + e.getMessage()); + } + }); + + // Wait for mode to fully initialize in the UI + Thread.sleep(300); + + // Step B: Now send the prompt after mode is ready + Display.getDefault().syncExec(() -> { + try { + ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); + if (commandService == null) return; + + Command command = commandService.getCommand(COPILOT_OPEN_CHAT_COMMAND); + if (command != null && command.isDefined() && command.isEnabled()) { + Map<String, String> promptParams = new HashMap<>(); + promptParams.put(PARAM_INPUT_VALUE, prompt); + promptParams.put(PARAM_AUTO_SEND, Boolean.TRUE.toString()); + + command.executeWithChecks(new ExecutionEvent(command, promptParams, null, null)); + } + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Prompt submission failed: " + e.getMessage()); + } + }); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + CxLogger.warning(LOG_PREFIX + " Automation sequence interrupted: " + e.getMessage()); + } + }); + + executionThread.start(); + return true; + + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Unexpected exception handling background dispatch: " + e.getMessage(), e); + return false; + } + } + + + /** + * 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..03d21816 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java @@ -0,0 +1,584 @@ +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` (**IMPORTANT:** If you detect the file is `bower.json`, use `bower install`, `bower list` instead)\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") + .append(" - `bower` (`bower.json`): `bower install`, `bower list`\n") + .append(" - `rubygems` (`Gemfile`): `bundle install`, `bundle exec rspec`\n") + .append(" - `composer` (`composer.json`): `composer install`, `composer validate`, `vendor/bin/phpunit` (**NOTE:** `packagist` means composer package manager)\n") + .append(" - `swift` (`Package.swift`): `swift build`, `swift test`\n") + .append(" - `cocoapods` (`Podfile`/`Podfile.lock`): `pod install --repo-update`, `xcodebuild test`\n") + .append(" - `carthage` (`Cartfile.resolved`): `carthage update --platform ios`, `carthage build`\n") + .append(" - `pub`/`dart` (`pubspec.yaml`/`pubspec.lock`): `dart pub get`, `dart test` (use `flutter pub get`, `flutter test` instead if this is a Flutter project)\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 <image>`\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/RemediationLinkHandler.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationLinkHandler.java new file mode 100644 index 00000000..45c676e8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationLinkHandler.java @@ -0,0 +1,466 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import java.util.List; +import java.util.Objects; + +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.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.telemetry.TelemetryService; +import com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView; + +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.SEPERATOR; +import static java.lang.String.format; + +/** + * Handler for remediation actions triggered from tooltips in the editor. + * This class processes remediation links extracted from hover popups and executes + * the corresponding actions such as fixing issues, viewing details, or ignoring + * certain types of issues. + * + * Adapted from JetBrains IntelliJ implementation to work with Eclipse's link + * handling mechanism via browser LocationListener. + */ +public class RemediationLinkHandler { + + private static final String FIX = "copyfixprompt"; + private static final String VIEW_DETAILS = "viewdetails"; + private static final String IGNORE_THIS_TYPE = "ignorethis"; + private static final String IGNORE_ALL_OF_THIS_TYPE = "ignoreallofthis"; + + private final RemediationManager remediationManager = new RemediationManager(); + + /** + * Handles a remediation link with a provided scan issue context. + * This is the primary entry point when a link is clicked in the hover popup. + * + * @param link the link string containing action and issue information, + * formatted as: action|issueId|engineName + * @param scanIssue the scan issue context for the remediation action + * @return true if the link was handled successfully, false otherwise. + */ + public boolean handleLink(@NonNull String link, @NonNull ScanIssue scanIssue) { + if (!link.contains(SEPERATOR)) { + CxLogger.warning("RTS-Fix: Remediation action failed, Link is not valid: " + link); + return false; + } + + String[] linkData = link.split(SEPERATOR); + String scanIssueId = extractIssueId(linkData); + if (scanIssueId.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan issue id not found in remediation link: " + link); + return false; + } + + String action = extractAction(linkData); + if (action.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Action not found in remediation link: " + link); + return false; + } + + // Note: the scanIssue is already known here, so unlike the other handleLink() + // overload, a missing engine-name segment (e.g. links built without it, such as + // "ignorethis"/"ignoreallofthis") must not block the action from running. + String engineName = extractEngineName(linkData); + + CxLogger.info(format("RTS-Fix: %s Remediation action called for engine: %s with issue id: %s", action, engineName, scanIssueId)); + + return handleActions(action, scanIssue, scanIssueId); + } + + /** + * Handles a remediation link by extracting action information and searching for the issue. + * This is an alternative entry point when the scan issue is not readily available + * and must be retrieved from the ProblemHolderService. + * + * @param link the link string containing action and issue information, + * formatted as: action|issueId|engineName + * @return true if the link was handled successfully, false otherwise. + */ + public boolean handleLink(@NonNull String link) { + if (!link.contains(SEPERATOR)) { + CxLogger.warning("RTS-Fix: Remediation action failed, Link is not valid: " + link); + return false; + } + + String[] linkData = link.split(SEPERATOR); + String scanIssueId = extractIssueId(linkData); + if (scanIssueId.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan issue id not found in remediation link: " + link); + return false; + } + + String action = extractAction(linkData); + if (action.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Action not found in remediation link: " + link); + return false; + } + + String engineName = extractEngineName(linkData); + if (Objects.isNull(engineName) || engineName.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan engine name not found in remediation link: " + link); + return false; + } + + CxLogger.info(format("RTS-Fix: %s Remediation action called for engine: %s with issue id: %s", action, engineName, scanIssueId)); + + ScanIssue scanIssue = getScanIssue(scanIssueId, engineName); + if (Objects.isNull(scanIssue)) { + CxLogger.warning(format("RTS-Fix: %s Remediation action failed. Scan issue is not found for the given issue-id: %s", action, scanIssueId)); + return false; + } + + return handleActions(action, scanIssue, scanIssueId); + } + + /** + * Handles specific remediation actions for a given scan issue. + * Depending on the provided action link, it performs appropriate actions + * such as applying a fix, viewing issue details, or ignoring the issue type. + * + * @param action the remediation action to be performed + * @param scanIssue the scan issue on which the action is performed + * @param actionId the action ID for vulnerability-specific fixes + * @return true if the action is successfully handled, false otherwise + */ + private boolean handleActions(@NonNull String action, @NonNull ScanIssue scanIssue, @NonNull String actionId) { + switch (action) { + case FIX: + remediationManager.fixWithCxOneAssist(scanIssue, actionId); + TelemetryService.logFixWithCxOneAssistAction(scanIssue); + break; + case VIEW_DETAILS: + remediationManager.viewDetails(scanIssue, actionId); + TelemetryService.logViewDetailsAction(scanIssue); + break; + case IGNORE_THIS_TYPE: { + IProject project = getActiveProject(); + if (Objects.isNull(project)) { + CxLogger.warning("RTS-Fix: Remediation action failed, no active project found for IGNORE_THIS_TYPE"); + return false; + } + com.checkmarx.eclipse.devassist.ignore.IgnoreManager.getInstance(project).addIgnoredEntry(scanIssue, actionId); + TelemetryService.logIgnorePackageAction(scanIssue); + deleteMarkerForIssue(project, scanIssue); + refreshFindingsView(); + com.checkmarx.eclipse.devassist.ui.findings.ignore.DevAssistIgnoredFindings.refreshIfOpen(); + break; + } + case IGNORE_ALL_OF_THIS_TYPE: { + IProject project = getActiveProject(); + if (Objects.isNull(project)) { + CxLogger.warning("RTS-Fix: Remediation action failed, no active project found for IGNORE_ALL_OF_THIS_TYPE"); + return false; + } + com.checkmarx.eclipse.devassist.ignore.IgnoreManager.getInstance(project).addAllIgnoredEntry(scanIssue, actionId); + TelemetryService.logIgnoreAllAction(scanIssue); + deleteMarkersForAllMatches(project, scanIssue); + refreshFindingsView(); + com.checkmarx.eclipse.devassist.ui.findings.ignore.DevAssistIgnoredFindings.refreshIfOpen(); + break; + } + default: + CxLogger.warning(format("RTS-Fix: Remediation action %s is not supported", action)); + return false; + } + return true; + } + + /** + * Resolves the active project the same way {@code CxFindingsView} does + * (single-project workspace assumption used throughout the ignore/revive feature). + */ + @Nullable + private org.eclipse.core.resources.IProject getActiveProject() { + org.eclipse.core.resources.IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (org.eclipse.core.resources.IProject project : projects) { + if (project.isOpen()) { + return project; + } + } + return null; + } + + /** + * Extracts the engine name from the link data array. + * + * @param linkData split link data array + * @return scan engine name, or empty string if not found + */ + private String extractEngineName(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 2 ? linkData[2] : ""; + } + + /** + * Extracts the issue id from the link data array. + * + * @param linkData split link data array + * @return scan issue id, or empty string if not found + */ + private String extractIssueId(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 1 ? linkData[1] : ""; + } + + /** + * Extracts the action from the link data array. + * + * @param linkData split link data array + * @return remediation action, or empty string if not found + */ + private String extractAction(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 0 ? linkData[0] : ""; + } + + /** + * Retrieves a specific scan issue based on the provided issue ID and engine name. + * Queries the ProblemHolderService to find the matching issue by either scan issue ID + * or vulnerability ID. This method iterates through all cached scan issues to find + * a match. + * + * @param issueId the unique identifier of the scan issue to retrieve + * @param engineName the scan engine name to match + * @return the {@link ScanIssue} matching the given issue ID and engine, or null if not found + */ + @Nullable + private ScanIssue getScanIssue(@NonNull String issueId, @NonNull String engineName) { + try { + CxLogger.warning("RTS-Fix: Searching for scan issue with ID: " + issueId + ", engine: " + engineName); + + // Get all cached scan issues from all files + java.util.Map<String, List<ScanIssue>> allIssuesMap = getAllCachedScanIssues(); + if (allIssuesMap == null || allIssuesMap.isEmpty()) { + CxLogger.warning("RTS-Fix: No scan issues found in cache to handle the link"); + return null; + } + + // Flatten the map into a single list of all issues + List<ScanIssue> allIssues = new java.util.ArrayList<>(); + for (List<ScanIssue> issueList : allIssuesMap.values()) { + allIssues.addAll(issueList); + } + + ScanIssue scanIssue = getScanIssueUsingScanIssueId(allIssues, issueId, engineName); + if (Objects.isNull(scanIssue)) { + return getScanIssueUsingVulnerabilityId(allIssues, issueId, engineName); + } + return scanIssue; + } catch (Exception exception) { + CxLogger.warning("RTS-Fix: Exception occurred while retrieving scan issue"); + return null; + } + } + + /** + * Helper method to get all cached scan issues from the workspace. + * Since ProblemHolderService is project-scoped, we try to find a cached instance + * or return an empty map if none are available. + * + * @return Map of all cached scan issues (file path → list of issues) + */ + @Nullable + private java.util.Map<String, List<ScanIssue>> getAllCachedScanIssues() { + try { + // Try to get issues from Eclipse workspace root (project-agnostic approach) + // In a multi-project workspace, this will only get issues from the currently + // active project's ProblemHolderService instance. For a complete solution, + // iterate through all open projects (requires org.eclipse.core.resources.IWorkspace) + org.eclipse.core.resources.IWorkspaceRoot root = + org.eclipse.core.resources.ResourcesPlugin.getWorkspace().getRoot(); + org.eclipse.core.resources.IProject[] projects = root.getProjects(); + + java.util.Map<String, List<ScanIssue>> combinedIssues = new java.util.HashMap<>(); + + for (org.eclipse.core.resources.IProject project : projects) { + try { + if (project.isOpen()) { + ProblemHolderService service = ProblemHolderService.getInstance(project); + if (service != null) { + java.util.Map<String, List<ScanIssue>> projectIssues = service.getAllScanIssues(); + if (projectIssues != null) { + combinedIssues.putAll(projectIssues); + } + } + } + } catch (Exception e) { + // Skip projects with errors, continue with others + CxLogger.warning("RTS-Fix: Error accessing project " + project.getName()); + } + } + + return combinedIssues; + } catch (Exception e) { + CxLogger.warning("RTS-Fix: Error retrieving cached scan issues from workspace"); + return null; + } + } + + /** + * Retrieves the ScanIssue corresponding to the given scan issue ID from the provided list. + * Matches both the issue ID and the scan engine name. + * + * @param scanIssueList list of scan issues to search + * @param issueId scan issue id to match + * @param engineName scan engine name to match + * @return the ScanIssue matching the given issueId and engine, or null if no match is found + */ + @Nullable + private ScanIssue getScanIssueUsingScanIssueId(@NonNull List<ScanIssue> scanIssueList, + @NonNull String issueId, + @NonNull String engineName) { + return scanIssueList.stream() + .filter(issue -> Objects.nonNull(issue) + && issue.getScanIssueId().equals(issueId) + && issue.getScanEngine().name().equalsIgnoreCase(engineName)) + .findFirst() + .orElse(null); + } + + /** + * Retrieves the ScanIssue by searching through vulnerabilities of scan issues. + * Used when a vulnerability ID is provided instead of a scan issue ID. + * Matches both the vulnerability ID and the scan engine name. + * + * @param scanIssueList list of scan issues to search + * @param issueId vulnerability id to match + * @param engineName scan engine name to match + * @return the ScanIssue containing the matching vulnerability, or null if not found + */ + @Nullable + private ScanIssue getScanIssueUsingVulnerabilityId(@NonNull List<ScanIssue> scanIssueList, + @NonNull String issueId, + @NonNull String engineName) { + for (ScanIssue scanIssue : scanIssueList) { + if (Objects.nonNull(scanIssue) && scanIssue.getScanEngine().name().equalsIgnoreCase(engineName) + && Objects.nonNull(scanIssue.getVulnerabilities()) && !scanIssue.getVulnerabilities().isEmpty()) { + for (var vulnerability : scanIssue.getVulnerabilities()) { + if (vulnerability.getVulnerabilityId().equals(issueId)) { + return scanIssue; + } + } + } + } + return null; + } + + /** + * Deletes markers for a single ignored issue from the editor. + * Finds the file where the issue is located and removes the marker. + * + * @param project the active project + * @param scanIssue the scan issue to find and delete markers for + */ + private void deleteMarkerForIssue(@NonNull IProject project, @NonNull ScanIssue scanIssue) { + try { + String filePath = scanIssue.getFilePath(); + if (filePath == null || filePath.isEmpty()) { + CxLogger.warning("RTS-Fix: Cannot delete marker, scan issue has no file path"); + return; + } + + // scanIssue.getFilePath() is an absolute OS path (same format as + // file.getLocation().toOSString() elsewhere in this codebase), not a + // project-relative path - IProject.getFile(String) expects the latter and + // would never resolve here, silently no-oping this whole method. + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); + if (file == null || !file.exists()) { + CxLogger.warning("RTS-Fix: Cannot delete marker, file not found: " + filePath); + return; + } + + IMarker[] markers = file.findMarkers("com.checkmarx.eclipse.plugin.checkmarxProblemMarker", true, IResource.DEPTH_ZERO); + for (IMarker marker : markers) { + try { + // Match against the actual attribute key MarkerIssueMapper stores the + // issue id under ("cx.issueId") - "scanIssueId" was never a real + // attribute on these markers, so this comparison always failed. + String markerScanIssueId = com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper + .getIssueId(marker); + if (markerScanIssueId.equals(scanIssue.getScanIssueId())) { + marker.delete(); + CxLogger.info("RTS-Fix: Deleted marker for issue: " + scanIssue.getTitle()); + return; + } + } catch (Exception e) { + CxLogger.warning("RTS-Fix: Error checking marker attribute: " + e.getMessage()); + } + } + } catch (Exception e) { + CxLogger.error("RTS-Fix: Error deleting marker for ignored issue", e); + } + } + + /** + * Deletes all markers for issues matching the ignore-all criteria. + * For OSS/CONTAINERS, finds all markers with the same package/image. + * + * @param project the active project + * @param scanIssue the scan issue to match against + */ + private void deleteMarkersForAllMatches(@NonNull IProject project, @NonNull ScanIssue scanIssue) { + try { + IMarker[] allMarkers = project.findMarkers("com.checkmarx.eclipse.plugin.checkmarxProblemMarker", true, IResource.DEPTH_INFINITE); + int deletedCount = 0; + + for (IMarker marker : allMarkers) { + try { + String markerTitle = marker.getAttribute(IMarker.MESSAGE, ""); + if (markerTitle.contains(scanIssue.getTitle())) { + marker.delete(); + deletedCount++; + } + } catch (Exception e) { + CxLogger.warning("RTS-Fix: Error checking marker for deletion: " + e.getMessage()); + } + } + + if (deletedCount > 0) { + CxLogger.info("RTS-Fix: Deleted " + deletedCount + " markers for ignore-all action"); + } + } catch (Exception e) { + CxLogger.error("RTS-Fix: Error deleting markers for ignore-all", e); + } + } + + /** + * Refreshes the Findings View to update the tree and remove ignored findings. + * Finds the CxFindingsView instance and calls refreshTreeWithFilter() on it. + */ + private void refreshFindingsView() { + try { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window == null) { + CxLogger.warning("RTS-Fix: Cannot refresh findings view, no active workbench window"); + return; + } + + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + CxLogger.warning("RTS-Fix: Cannot refresh findings view, no active workbench page"); + return; + } + + CxFindingsView view = (CxFindingsView) page.findView(CxFindingsView.ID); + if (view == null) { + CxLogger.warning("RTS-Fix: Cannot refresh findings view, view not found"); + return; + } + + view.refreshTreeWithFilter(); + CxLogger.info("RTS-Fix: Refreshed Findings View"); + } catch (Exception e) { + CxLogger.error("RTS-Fix: Error refreshing findings view", e); + } + } +} 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..d5d9c060 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java @@ -0,0 +1,405 @@ +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 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; +import com.checkmarx.eclipse.devassist.utils.PackageManager; + +/** + * RemediationManager provides remediation options for issues identified during + * a real-time scan. + * <p> + * This class supports applying fixes, viewing details etc. for scan issues + * detected by different scan engines, such as OSS, ASCA, etc. + * <p> + * Main responsibilities: + * <ul> + * <li>Apply remediation for different scan engine issues</li> + * <li>Generate and copy remediation prompts to the clipboard</li> + * <li>Log remediation actions</li> + * </ul> + */ +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. + * <p> + * This method attempts to: + * <ol> + * <li>Open GitHub Copilot Chat</li> + * <li>Switch to Agent mode</li> + * <li>Paste and send the prompt automatically</li> + * </ol> + * <p> + * 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, "Agent"); + } catch (Exception exception) { + CxLogger.error("RTS-Fix: Failed to fix with AI: ", exception); + return false; + } + } + + private boolean explainWithAI(@NonNull String prompt) { + try { + return CopilotIntegration.sendPromptToCopilot(prompt, "Ask"); + } catch (Exception exception) { + CxLogger.error("RTS-ViewDetails: Failed to explain 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 in Ask mode (no notifications shown by explainWithAI) + boolean aiSuccess = explainWithAI(prompt); + if (aiSuccess) { + CxLogger.info(format("RTS-ViewDetails: %s explanation sent to Copilot in Ask mode 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(), + PackageManager.mapToRemediationFormat(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<Vulnerability> 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<Vulnerability> 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..f647fa22 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java @@ -0,0 +1,362 @@ +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.ignore.IgnoreEntry; +import com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +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.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.common.utils.CxLogger; +import org.eclipse.core.resources.IProject; +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<Object> { + + private static final String LOG_TAG = "[ASCA-ADAPTOR]"; + + private final com.checkmarx.ast.asca.ScanResult ascaScanResult; + private final String filePath; + private final IProject project; + private final List<ScanIssue> 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 + * @param project the project the file belongs to, used to filter out + * already-ignored vulnerabilities - ASCA's CLI has no + * ignore-file exclusion of its own (see + * AscaScannerService#getIgnoreFilePath), so this app-level + * filtering is the only enforcement point + */ + public AscaScanResultAdaptor(com.checkmarx.ast.asca.ScanResult ascaScanResult, String filePath, IProject project) { + this(ascaScanResult, filePath, project, true); + } + + /** + * Constructs an instance of AscaScanResultAdaptor with optional ignore filtering. + * + * @param ascaScanResult the ASCA scan results to be wrapped + * @param filePath the path of the file being scanned + * @param project the project the file belongs to, used to filter out + * already-ignored vulnerabilities + * @param filterIgnored whether to filter out already-ignored vulnerabilities + * (true = filter, false = keep all). Pass false when + * building the adaptor used to reconcile ignored + * entries' line numbers, since that pass needs every + * vulnerability - including already-ignored ones - to + * track which occurrences are still present in the code. + */ + public AscaScanResultAdaptor(com.checkmarx.ast.asca.ScanResult ascaScanResult, String filePath, IProject project, + boolean filterIgnored) { + this.ascaScanResult = ascaScanResult; + this.filePath = filePath; + this.project = project; + this.scanIssues = filterIgnored ? buildIssues() : buildIssuesUnfiltered(); + } + + @Override + public com.checkmarx.ast.asca.ScanResult getResults() { + return ascaScanResult; + } + + @Override + public List<ScanIssue> getIssues() { + return scanIssues; + } + + /** + * Builds a list of ScanIssue objects from the ASCA scan results, filtering out + * already-ignored vulnerabilities. + * Groups multiple vulnerabilities on the same line and sorts them by severity. + */ + private List<ScanIssue> buildIssues() { + return buildIssuesInternal(true); + } + + /** + * Builds a list of ScanIssue objects from the ASCA scan results WITHOUT filtering + * ignored vulnerabilities. Used to reconcile ignored entries' line numbers, where all + * vulnerabilities (including already-ignored ones) are needed to track which occurrences + * are still present in the code. + */ + private List<ScanIssue> buildIssuesUnfiltered() { + return buildIssuesInternal(false); + } + + /** + * Groups multiple vulnerabilities on the same line and sorts them by severity. + * + * @param applyFilter true to filter out already-ignored vulnerabilities, false to keep all + */ + private List<ScanIssue> buildIssuesInternal(boolean applyFilter) { + if (ascaScanResult == null || ascaScanResult.getScanDetails() == null) { + CxLogger.info(LOG_TAG + " No scan results or scan details available"); + return Collections.emptyList(); + } + + List<ScanDetail> scanDetails = ascaScanResult.getScanDetails(); + if (scanDetails.isEmpty()) { + return Collections.emptyList(); + } + + // Group scan details by line number, then sort by severity precedence + Map<Integer, List<ScanDetail>> groupedIssues = scanDetails.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy( + ScanDetail::getLine, + Collectors.collectingAndThen(Collectors.toList(), detailsList -> { + detailsList.sort( + Comparator.comparingInt((ScanDetail detail) -> getSeverityPrecedence(detail.getSeverity())).reversed()); + return detailsList; + }))); + + // ASCA's CLI has no ignore-file exclusion of its own (see + // AscaScannerService#getIgnoreFilePath), so already-ignored vulnerabilities must be + // filtered out here, at the point ScanIssues are built - fetched once per file build + // rather than per group, since it's the same snapshot for every line in this file. + IgnoreManager ignoreManager = (applyFilter && project != null) ? IgnoreManager.getInstance(project) : null; + List<IgnoreEntry> ignoreEntries = (applyFilter && project != null) + ? IgnoreFileManager.getInstance(project).getAllIgnoreEntries() : Collections.emptyList(); + + List<ScanIssue> issues = groupedIssues.values().stream() + .map(detailList -> createScanIssueForGroup(detailList, ignoreManager, ignoreEntries)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + (applyFilter ? " Converted " : " Converted (unfiltered) ") + 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, filtering out any vulnerability already recorded as ignored - + * individually, by rule name + problematic line - rather than dropping or + * keeping the whole group, so ignoring one vulnerability on a multi-vulnerability + * line never hides the others on that line. + * + * @param ascaScanDetails the list of ASCA scan details for the same line + * (already sorted by severity) + * @param ignoreManager resolves whether a given vulnerability is ignored, or + * {@code null} if no project context is available or filtering + * is disabled + * @param ignoreEntries the current ignore entries to check against + * @return a ScanIssue representing the ASCA finding(s), or null if every + * vulnerability in the group is ignored or conversion fails + */ + private ScanIssue createScanIssueForGroup(List<ScanDetail> ascaScanDetails, IgnoreManager ignoreManager, + List<IgnoreEntry> ignoreEntries) { + if (ascaScanDetails == null || ascaScanDetails.isEmpty()) { + return null; + } + + try { + ScanIssue scanIssue = getScanIssue(ascaScanDetails); + + // Add vulnerabilities from all details in the group, skipping ones already ignored + 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); + + if (ignoreManager != null && ignoreManager.isAscaVulnerabilityIgnored(vuln, ignoreEntries, filePath)) { + CxLogger.info(LOG_TAG + " Skipping ignored vulnerability '" + vuln.getTitle() + + "' on line " + detail.getLine()); + continue; + } + scanIssue.getVulnerabilities().add(vuln); + } + + // If every vulnerability on this line is ignored, drop the ScanIssue entirely + if (scanIssue.getVulnerabilities().isEmpty()) { + return null; + } + + // 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<ScanDetail> 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() + DevAssistConstants.MULTIPLE_ASCA_ISSUES; + } 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())); + vulnerability.setRuleId(scanDetail.getRuleID()); + vulnerability.setProblematicLine(scanDetail.getProblematicLine()); + + 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<ScanDetail> 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() + DevAssistConstants.MULTIPLE_ASCA_ISSUES); + } + + // 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 "malicious": + return 6; + 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<Object> 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..5b2ad7e1 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -0,0 +1,392 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +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 com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +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.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; + +/** + * 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<ScanResult> { + + private static final String LOG_TAG = "[ASCA-SERVICE]"; + private static final String ASCA_DIR = "CxASCA"; + private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + 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<ScanResult> scan(String filePath) { + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + com.checkmarx.eclipse.devassist.common.ScanResult<Object> result = scanWithDocument(filePath, + liveDocument != null ? liveDocument : new Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult<ScanResult>) (com.checkmarx.eclipse.devassist.common.ScanResult<?>) result; + } + + /** + * Primary scan method - gets file content and executes scan. + */ + public com.checkmarx.eclipse.devassist.common.ScanResult<Object> 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<Object> 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; + } + com.checkmarx.ast.asca.ScanResult ascaScanResult = (com.checkmarx.ast.asca.ScanResult) rawResults; + // Update line numbers for ignored ASCA issues if any exist + updateIgnoredFileDataOnLatestResult(ascaScanResult, filePath, proj); + return new AscaScanResultAdaptor(ascaScanResult, filePath, proj); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Scan failed: " + e.getMessage(), e); + return null; + } + } + + /** + * Reconciles ignored ASCA entries' line numbers against this scan's raw results, and removes + * ignore entries whose finding is no longer present (e.g. the flagged code was deleted). If a + * user edits a file above an ignored finding, its line shifts - without this, the gutter + * icon/marker for that ignored finding would render at its stale line. + * <p> + * Unlike the other scanners, no second ("full") scan is needed here: ASCA's CLI invocation + * never actually excludes ignored findings (see {@link #getIgnoreFilePath}), so + * {@code rawResults} already contains every vulnerability, including already-ignored ones. + */ + private void updateIgnoredFileDataOnLatestResult(com.checkmarx.ast.asca.ScanResult rawResults, String filePath, + IProject proj) { + try { + IgnoreManager ignoreManager = IgnoreManager.getInstance(proj); + if (!ignoreManager.hasIgnoredEntries(ScanEngine.ASCA)) { + return; + } + if (rawResults != null && rawResults.getScanDetails() != null && !rawResults.getScanDetails().isEmpty()) { + AscaScanResultAdaptor unfilteredAdaptor = new AscaScanResultAdaptor(rawResults, filePath, proj, false); + ignoreManager.updateLineNumbersForIgnoredEntriesByProblematicLine(unfilteredAdaptor, filePath); + } else { + ignoreManager.removeIgnoreEntriesForFileIfEmpty(filePath); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored ASCA line numbers: " + + e.getMessage()); + } + } + + /** + * 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(project); + 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. + * Note: matching the JetBrains plugin's behavior, ASCA's CLI invocation does + * not currently accept a working ignore-file exclusion, so this path is + * resolved but intentionally not forwarded to ScanAsca() below - ASCA-ignored + * findings are instead excluded at the app level via + * IgnoreManager/IgnoreFileManager#isIgnored() in the findings tree/decoration + * filtering pipeline. + */ + private String getIgnoreFilePath(IProject project) { + return com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getIgnoreFilePath(project); + } + + /** + * 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 = wrapperProvider.scanAsca(path, ascaLatestVersion, agent, null); + } catch (IOException e) { + CxLogger.warning(String.format("%s IOException occurred while ASCA scan for file %s: %s", LOG_TAG, path, e.getMessage())); + } catch (InterruptedException e) { + CxLogger.warning(String.format("%s InterruptedException occurred while ASCA scan for file %s: %s", LOG_TAG, path, e.getMessage())); + } catch (CxException e) { + CxLogger.warning(String.format("%s CxException occurred while ASCA scan for file %s: %s", LOG_TAG, path, e.getMessage())); + } catch (Exception e) { + CxLogger.warning(String.format("%s Exception occurred while ASCA scan for file %s: %s", LOG_TAG, path, e.getMessage())); + } + 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<ContainersRealtimeResults> { + + 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<ScanIssue> 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<ScanIssue> getIssues() { + return scanIssues; + } + + /** + * Processes images obtained from the scan results and converts them into standardized scan issues. + */ + public List<ScanIssue> buildIssues() { + List<ContainersRealtimeImage> 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<ContainersRealtimeResults> 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..6a8bb52a --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -0,0 +1,306 @@ +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.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +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 + * WrapperProvider. + */ +public class ContainerScannerService extends BaseScannerService<ContainersRealtimeResults> { + + private static final String LOG_TAG = "[CONTAINER-SERVICE]"; + private static final String CONTAINER_DIR = "CxContainer"; + private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + private static final List<String> CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile*", + "**/*.containerfile", + "**/*.image", + "**/docker-compose*.yml", + "**/docker-compose*.yaml"); + + private static final List<String> 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<PathMatcher> 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<ContainersRealtimeResults> 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 = wrapperProvider.containersRealtimeScan(tempFilePath.toString(), ignoreFilePath); + } 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. If a user edits a file above an ignored container finding, its line shifts - + * without this, the gutter icon/marker for that ignored finding would render at its + * stale line. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { + try { + IgnoreManager ignoreManager = IgnoreManager.getInstance(proj); + if (!ignoreManager.hasIgnoredEntries(com.checkmarx.eclipse.devassist.utils.ScanEngine.CONTAINERS)) { + return; + } + CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); + ContainersRealtimeResults fullScanResults = wrapperProvider.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 container 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<ContainersRealtimeResults> 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..f449549f --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -0,0 +1,262 @@ +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.DevAssistConstants; +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<IacRealtimeResults> { + + private static final String LOG_TAG = "[IAC-ADAPTOR]"; + + private final IacRealtimeResults iacRealtimeResults; + private final String filePath; + private final List<ScanIssue> 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<ScanIssue> getIssues() { + return scanIssues; + } + + private List<ScanIssue> buildIssues() { + if (iacRealtimeResults == null || iacRealtimeResults.getResults() == null) { + CxLogger.info(LOG_TAG + " No scan results available"); + return Collections.emptyList(); + } + + List<IacRealtimeResults.Issue> issues = iacRealtimeResults.getResults(); + if (issues.isEmpty()) { + return Collections.emptyList(); + } + + // Group issues by line number, then sort by severity precedence + Map<Integer, List<IacRealtimeResults.Issue>> 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((IacRealtimeResults.Issue issue) -> getSeverityPrecedence(issue.getSeverity())).reversed()); + return issuesList; + }))); + + List<ScanIssue> 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<IacRealtimeResults.Issue> 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<IacRealtimeResults.Issue> 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() + DevAssistConstants.MULTIPLE_IAC_ISSUES; + } 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())); + vulnerability.setActualValue(iacIssue.getActualValue()); + vulnerability.setExpectedValue(iacIssue.getExpectedValue()); + // Per-instance discriminator: lets the ignore key tell apart multiple IaC + // vulnerabilities grouped onto the same line (see IgnoreManager#createJsonKeyForIgnoreEntry) + vulnerability.setSimilarityId(iacIssue.getSimilarityId()); + + CxLogger.info(LOG_TAG + " Created vulnerability '" + iacIssue.getTitle() + + "' with vulnerabilityId '" + vulnerabilityId + "'"); + + return vulnerability; + } + + private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List<IacRealtimeResults.Issue> 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() + DevAssistConstants.MULTIPLE_IAC_ISSUES); + } + + // 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 "malicious": + return 6; + 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<IacRealtimeResults> 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..a8db03ef --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -0,0 +1,325 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +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 com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +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.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * 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<IacRealtimeResults> { + + 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(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + // Supported glob patterns for IaC files + private static final List<String> 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<String> 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<PathMatcher> 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<IacRealtimeResults> scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + Path tempFolderPath = getSecureTempDirectory(); + Pair<Path, Path> 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 = DevAssistUtils.getContainerTool(); + String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj); + + IacRealtimeResults scanResults = null; + try { + scanResults = wrapperProvider + .iacRealtimeScan(tempFilePath, containerTool, ignoreFilePath); + } catch (Exception e) { + // TODO Auto-generated catch block + CxLogger.warning(String.format("%s Exception occurred while IAC scan for file %s: %s", LOG_TAG, filePath, e.getMessage())); + } + + if (scanResults == null) { + return null; + } + + IacScanResultAdaptor scanResultAdaptor = new IacScanResultAdaptor(scanResults, filePath); + + // Update line numbers for ignored IaC issues if any exist + updateIgnoredFileDataOnLatestResult(tempFilePath, proj, filePath, containerTool); + + 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<IacRealtimeResults> 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); + } + + /** + * Re-runs the scan without the ignore file to reconcile ignored IaC issues' line numbers + * against a fresh, unfiltered result. If a user edits a file above an ignored IaC finding, + * its line shifts - without this, the gutter icon/marker for that ignored finding would + * render at its stale line. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath, + String containerTool) { + try { + IgnoreManager ignoreManager = IgnoreManager.getInstance(proj); + if (!ignoreManager.hasIgnoredEntries(com.checkmarx.eclipse.devassist.utils.ScanEngine.IAC)) { + return; + } + CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored IaC issues"); + IacRealtimeResults fullScanResults = wrapperProvider.iacRealtimeScan(tempFilePath, containerTool, ""); + if (fullScanResults != null) { + IacScanResultAdaptor fullScanResultAdaptor = new IacScanResultAdaptor(fullScanResults, filePath); + ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored IaC line numbers: " + + e.getMessage()); + } + } + + /** + * Saves file content to an isolated subfolder inside the temporary directory using a hashed name. + */ + private Pair<Path, Path> 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<Path, Path> 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; + } +} \ 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..398279a2 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java @@ -0,0 +1,208 @@ +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<OssRealtimeResults> { + + private static final String LOG_TAG = "[OSS-ADAPTOR]"; + + private final OssRealtimeResults ossRealtimeResults; + private final String filePath; + private final List<ScanIssue> 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<ScanIssue> 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<ScanIssue> buildIssues() { + List<OssRealtimeScanPackage> 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<ScanIssue> issues = packages.stream() + .map(this::createScanIssue) + .filter(Objects::nonNull) + .collect(Collectors.toMap(ScanIssue::getScanIssueId, java.util.function.Function.identity(), + (first, duplicate) -> first, java.util.LinkedHashMap::new)) + .values().stream() + .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..5c612ed9 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -0,0 +1,179 @@ +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.devassist.utils.PackageManager; +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<IFile> matchedFiles = new ArrayList<>(); + + List<PathMatcher> pathMatchers = PackageManager.getAllPatterns().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> 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<ScanIssue> 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<OssRealtimeResults> scan(String filePath, IDocument document) { + return ossScannerService.scanWithDocument(filePath, document); + } + + /** + * Execute scan on a file path directly. + */ + public ScanResult<OssRealtimeResults> 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..e7e43766 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -0,0 +1,312 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +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.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.PackageManager; + +/** + * 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<OssRealtimeResults> { + + private static final String LOG_TAG = "[OSS-SERVICE]"; + private static final String OSS_DIR = "CxOSS"; + private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + 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<PathMatcher> pathMatchers = PackageManager.getAllPatterns().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<OssRealtimeResults> 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<OssRealtimeResults> 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<String> 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); + + String ignoreFilePath = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getIgnoreFilePath(project); + OssRealtimeResults scanResults = wrapperProvider.ossRealtimeScan(mainTempPath.get(), ignoreFilePath); + if (scanResults == null) { + return null; + } + + OssScanResultAdaptor scanResultAdaptor = new OssScanResultAdaptor(scanResults, filePath); + + // Update line numbers for ignored packages if any exist + updateIgnoredFileDataOnLatestResult(mainTempPath.get(), 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); + } + } + } + + /** + * Re-runs the scan without the ignore file to reconcile ignored packages' line numbers + * against a fresh, unfiltered result. If a user edits a file above an ignored package + * finding, its line shifts - without this, the gutter icon/marker for that ignored finding + * would render at its stale line. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, String filePath) { + try { + IgnoreManager ignoreManager = IgnoreManager.getInstance(project); + if (!ignoreManager.hasIgnoredEntries(com.checkmarx.eclipse.devassist.utils.ScanEngine.OSS)) { + return; + } + CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); + OssRealtimeResults fullScanResults = wrapperProvider.ossRealtimeScan(tempFilePath, ""); + if (fullScanResults != null && fullScanResults.getPackages() != null) { + OssScanResultAdaptor fullScanResultAdaptor = new OssScanResultAdaptor(fullScanResults, filePath); + ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored OSS line numbers: " + + e.getMessage()); + } + } + + /** + * Persists the main manifest file into the temporary directory for scanning. + */ + private Optional<String> 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 companion lock files (e.g., package-lock.json, yarn.lock) into the temporary directory + * when they exist alongside the scanned manifest. + * + * @param tempFolderPath temp directory where companion files should be written + * @param originalFilePath original manifest path used to locate companion files + */ + 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(); + List<String> companionFileNameList = PackageManager.getCompanionFileNames(parentFileName); + + if (companionFileNameList.isEmpty()) { + return; + } + + Path parentPath = originalPath.getParent(); + if (parentPath == null) { + return; + } + for (String companionFileName : companionFileNameList) { + Path companionOriginalPath = parentPath.resolve(companionFileName); + if (!Files.exists(companionOriginalPath)) { + continue; + } + + 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()); + } + } + } + + /** + * 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; + } +} \ 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<SecretsRealtimeResults> { + + private final SecretsRealtimeResults secretsRealtimeResults; + private final String filePath; + private final List<ScanIssue> 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<ScanIssue> 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<ScanIssue> buildIssues() { + if (Objects.isNull(getResults())) { + return Collections.emptyList(); + } + + List<SecretsRealtimeResults.Secret> 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<SecretsRealtimeResults> 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..8dfa17ec --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -0,0 +1,299 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +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.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * 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<SecretsRealtimeResults> { + + private static final String LOG_TAG = "[SECRETS-SERVICE]"; + private static final String SECRETS_DIR = "CxSecrets"; + private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + // Glob patterns for manifest files that should be excluded from Secrets scanning + private static final List<String> 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<PathMatcher> 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<SecretsRealtimeResults> 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<String> 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 = DevAssistUtils.getIgnoreFilePath(proj); + + SecretsRealtimeResults scanResults = wrapperProvider + .secretsRealtimeScan(tempFilePath.get(), ignoreFilePath); + + 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<SecretsRealtimeResults> 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. If a user edits a file above an ignored secret, its line shifts - without this, + * the gutter icon/marker for that ignored secret would render at its stale line. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { + try { + IgnoreManager ignoreManager = IgnoreManager.getInstance(proj); + if (!ignoreManager.hasIgnoredEntries(com.checkmarx.eclipse.devassist.utils.ScanEngine.SECRETS)) { + return; + } + CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored secrets"); + SecretsRealtimeResults fullScanResults = wrapperProvider.secretsRealtimeScan(tempFilePath, ""); + if (fullScanResults != null) { + SecretsScanResultAdaptor fullScanResultAdaptor = new SecretsScanResultAdaptor(fullScanResults, filePath); + ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored secrets line numbers: " + + e.getMessage()); + } + } + + /** + * 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<String> 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; + } +} \ 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<ScanEngine, Boolean> 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<ScanEngine, Boolean> 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..0a8bac53 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java @@ -0,0 +1,107 @@ +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 { + + // Aligned to match the canonical plugin qualifier used across the plugin + private static final String PLUGIN_ID = "com.checkmarx.eclipse"; + // IMPORTANT: Must match the keys used in CheckmarxPreferencePage + private static final String KEY_PREFIX = "scanner."; + private static final String KEY_ENABLED_SUFFIX = ".enabled"; + private static final String KEY_FREQUENCY = "scan.frequency"; + private static final String KEY_USER_PREFERENCES_SET = "user.preferences.set"; + + 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()); + } + + /** + * Checks if user has explicitly configured scanner preferences. + * Returns true if ANY scanner preference has been explicitly set (stored in preferences). + * This is more reliable than checking a separate flag, as it detects actual customization. + */ + public boolean isUserPreferencesSet() { + // Check if the explicit flag is set (legacy behavior) + if (prefs.getBoolean(KEY_USER_PREFERENCES_SET)) { + return true; + } + + // Also check if ANY scanner preference has been explicitly set in the store + // This handles cases where user went directly to preferences page and configured scanners + for (ScanEngine engine : ScanEngine.values()) { + String key = getEnabledKey(engine); + // If this key exists in the preference store (has been explicitly set), preferences are set + if (prefs.contains(key)) { + return true; + } + } + + return false; + } + + public void setUserPreferencesSet(boolean set) { + prefs.setValue(KEY_USER_PREFERENCES_SET, set); + } + + 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/telemetry/TelemetryService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/telemetry/TelemetryService.java new file mode 100644 index 00000000..536e78f8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/telemetry/TelemetryService.java @@ -0,0 +1,200 @@ +package com.checkmarx.eclipse.devassist.telemetry; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.Platform; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.backend.Constants; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +import static java.lang.String.format; + +/** + * Telemetry service for logging user interactions with remediation actions + * (Fix with CxOne Assist, View Details, Ignore, Ignore All). + * Mirrors the JetBrains plugin's TelemetryService: every event is sent + * asynchronously via the Checkmarx CLI wrapper's telemetryAIEvent, and a + * failure to send is only logged - it never surfaces to the user or blocks + * the action being reported on. + */ +public final class TelemetryService { + + // AI Provider constant + private static final String AI_PROVIDER = "Copilot"; + + // Agent name base, matching JetBrains' "Jetbrains <IDE name>" pattern + private static final String ECLIPSE_AGENT_NAME = "Eclipse"; + + // Event Types + private static final String EVENT_TYPE_CLICK = "click"; + + // Sub Types + private static final String SUB_TYPE_FIX_WITH_AI_CHAT = "fixWithAIChat"; + private static final String SUB_TYPE_VIEW_DETAILS = "viewDetails"; + private static final String SUB_TYPE_IGNORE_PACKAGE = "ignorePackage"; + private static final String SUB_TYPE_IGNORE_ALL = "ignoreAll"; + + // Engine Names + private static final String ENGINE_OSS = "Oss"; + private static final String ENGINE_SECRETS = "Secrets"; + private static final String ENGINE_IAC = "IaC"; + private static final String ENGINE_ASCA = "Asca"; + private static final String ENGINE_CONTAINERS = "Containers"; + + private TelemetryService() { + } + + /** + * Sends a user-action ("click") telemetry event. + * + * @param eventType the type of event (e.g., "click") + * @param subType the specific action (e.g., "fixWithAIChat", "viewDetails") + * @param engine the scan engine type + * @param problemSeverity the severity of the issue + */ + public static void setUserEventDataForLogs(String eventType, String subType, String engine, String problemSeverity) { + CompletableFuture.runAsync(() -> { + try { + CxWrapperFactory.build().telemetryAIEvent( + AI_PROVIDER, // aiProvider + getAgentName(), // agent + eventType, // eventType + subType, // subType + engine, // engine + problemSeverity, // problemSeverity + "", // scanType + "", // status + 0 // totalCount + ); + } catch (Exception e) { + CxLogger.warning(format("Telemetry: Failed to log user event telemetry for %s - %s", subType, e.getMessage())); + } + }); + } + + /** + * Logs a user action for a given scan issue. + * + * @param scanIssue the scan issue being acted upon + * @param actionSubType the specific action sub-type for telemetry + * @param actionName the action name, used only for logging + */ + private static void logUserAction(ScanIssue scanIssue, String actionSubType, String actionName) { + if (Objects.isNull(scanIssue)) { + CxLogger.warning("Telemetry: Cannot log " + actionName + " action - scan issue is null"); + return; + } + + String engine = mapScanEngineToTelemetryEngine(scanIssue.getScanEngine()); + String severity = normalizeSeverity(scanIssue.getSeverity()); + + setUserEventDataForLogs(EVENT_TYPE_CLICK, actionSubType, engine, severity); + } + + /** + * Logs user action for "Fix with CxOne Assist". + * + * @param scanIssue the scan issue being acted upon + */ + public static void logFixWithCxOneAssistAction(ScanIssue scanIssue) { + logUserAction(scanIssue, SUB_TYPE_FIX_WITH_AI_CHAT, "Fix with CxOne Assist"); + } + + /** + * Logs user action for "View Details". + * + * @param scanIssue the scan issue being acted upon + */ + public static void logViewDetailsAction(ScanIssue scanIssue) { + logUserAction(scanIssue, SUB_TYPE_VIEW_DETAILS, "View Details"); + } + + /** + * Logs user action for "Ignore this vulnerability". + * + * @param scanIssue the scan issue being acted upon + */ + public static void logIgnorePackageAction(ScanIssue scanIssue) { + logUserAction(scanIssue, SUB_TYPE_IGNORE_PACKAGE, "Ignore Package"); + } + + /** + * Logs user action for "Ignore all of this type". + * + * @param scanIssue the scan issue being acted upon + */ + public static void logIgnoreAllAction(ScanIssue scanIssue) { + logUserAction(scanIssue, SUB_TYPE_IGNORE_ALL, "Ignore All"); + } + + /** + * Maps the model's ScanEngine enum to the telemetry engine string expected + * by the backend (matching JetBrains' naming exactly). + */ + private static String mapScanEngineToTelemetryEngine(ScanEngine scanEngine) { + if (Objects.isNull(scanEngine)) { + return ENGINE_OSS; // default fallback + } + + switch (scanEngine) { + case OSS: + return ENGINE_OSS; + case SECRETS: + return ENGINE_SECRETS; + case IAC: + return ENGINE_IAC; + case ASCA: + return ENGINE_ASCA; + case CONTAINERS: + return ENGINE_CONTAINERS; + default: + return ENGINE_OSS; // default fallback + } + } + + /** + * Normalizes severity strings to match the existing Constants format. + */ + private static String normalizeSeverity(String severity) { + if (Objects.isNull(severity) || severity.trim().isEmpty()) { + return Constants.UNKNOWN; + } + + switch (severity.toLowerCase().trim()) { + case "critical": + return Constants.CRITICAL_SEVERITY; + case "high": + return Constants.HIGH_SEVERITY; + case "medium": + return Constants.MEDIUM_SEVERITY; + case "low": + return Constants.LOW_SEVERITY; + case "malicious": + return Constants.MALICIOUS_SEVERITY; + default: + return Constants.UNKNOWN; + } + } + + /** + * Gets the agent name based on the running Eclipse platform version. + */ + private static String getAgentName() { + try { + String version = Platform.getBundle("org.eclipse.platform") != null + ? Platform.getBundle("org.eclipse.platform").getVersion().toString() + : null; + if (version != null) { + return ECLIPSE_AGENT_NAME + " " + version; + } + } catch (Exception e) { + CxLogger.warning("Telemetry: Could not determine Eclipse platform version, using default - " + e.getMessage()); + } + + return ECLIPSE_AGENT_NAME; + } +} 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..07f9562f --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -0,0 +1,1337 @@ +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.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 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.CheckmarxEditorListener; +import com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +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.ProblemDecorator; +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.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.telemetry.TelemetryService; +import com.checkmarx.eclipse.common.utils.CxLogger; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +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 { + + 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<String, List<ScanIssue>> currentIssues = new HashMap<>(); + // Files that had at least one active (non-ignored) finding as of the last + // applyDecorationsToOpenEditors() pass. Used to detect the "file just lost its + // last active finding" transition without having to re-decorate every file + // ever tracked in currentIssues on every single refresh - see + // applyDecorationsToOpenEditors() for details. + private Set<String> lastDecoratedFiles = new HashSet<>(); + private IgnoreFileManager ignoreFileManager; + private IProject currentProject; + 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(); + private static final Image STAR_ICON = AbstractUIPlugin + .imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, "/icons/severity/star-action.svg").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(); + + // Initialize ignore file manager lazily when needed + ensureIgnoreFileManagerInitialized(); + + // 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 (!Preferences.isAuthenticated()) { + 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]; + // Use getInstance() to get the correct ProblemHolderService instance + // (matches the same key used by getInstance() in ProblemHolderService) + ProblemHolderService problemHolder = ProblemHolderService.getInstance(project); + + if (problemHolder != null) { + Map<String, List<ScanIssue>> existingIssues = problemHolder.getAllScanIssues(); + if (existingIssues != null && !existingIssues.isEmpty()) { + this.currentIssues = existingIssues; + } + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error reading cached issues: " + e.getMessage()); + } + } + + /** + * Ensures IgnoreFileManager is initialized. Called lazily to handle + * cases where projects aren't available at view creation time. + */ + private void ensureIgnoreFileManagerInitialized() { + if (ignoreFileManager == null) { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects.length > 0) { + currentProject = projects[0]; + ignoreFileManager = IgnoreFileManager.getInstance(currentProject); + } + } + } + + /** + * Resolves the owning IProject for a file path. + * Returns the project that owns the file, or null if file not found. + * This per-file resolution replaces the stale currentProject logic. + */ + private IProject resolveProjectFromFilePath(String filePath) { + try { + if (filePath == null || filePath.isEmpty()) { + return null; + } + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); + if (file != null && file.exists()) { + return file.getProject(); + } + } catch (Exception e) { + CxLogger.warning("Failed to resolve project for file: " + filePath); + } + return null; + } + + private Composite openSettingsComposite; + + /** + * Renders the missing credentials panel centered inside the view parent. + * Also clears all findings from the ProblemHolderService and editor annotations on logout. + */ + private void drawMissingCredentialsPanel(Composite parent) { + // Clear all findings from memory BEFORE disposing UI (to ensure tab title updates) + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = ProblemHolderService.getInstance(project); + if (problemHolder != null) { + problemHolder.clearAll(); + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error clearing findings on logout: " + e.getMessage()); + } + + // Reset current issues cache + currentIssues.clear(); + + // Update tab title immediately to remove problem count + setPartName(DevAssistConstants.DEVASSIST_TAB); + + // Clear all annotations from open editors + try { + com.checkmarx.eclipse.devassist.problems.ProblemDecorator.clearAllAnnotations(); + } catch (Exception e) { + System.err.println("[FINDINGS] Error clearing annotations on logout: " + e.getMessage()); + } + + // 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()); + + Tree tree = new Tree(treeComposite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + treeViewer = new TreeViewer(tree); + treeViewer.setContentProvider(new FindingsContentProvider()); + treeViewer.setLabelProvider(new FindingsLabelProvider()); + + Composite promotionalComposite = new Composite(sashForm, SWT.NONE); + drawPromotionalPanel(promotionalComposite); + + sashForm.setWeights(new int[] { 70, 30 }); + + setupTreeListeners(); + + if (!currentIssues.isEmpty()) { + refreshTreeWithFilter(); + } + + parent.layout(true, true); + + // Setup toolbar after layout has settled to ensure proper rendering in tab bar + Display.getDefault().asyncExec(this::setupToolbar); + } + + /** + * 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<String, List<ScanIssue>> newIssues = (Map<String, List<ScanIssue>>) 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()); + } + } + + super.dispose(); + } + + private Composite parentComposite; + + /** + * 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(); + + // Force layout update on the parent to ensure toolbar renders properly + if (parentComposite != null && !parentComposite.isDisposed()) { + parentComposite.layout(true, true); + } + } + + 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 IgnoreFileManager and appears in the Ignored + * Findings Window. + */ + private void ignoreThisFinding(ScanIssue issue) { + try { + if (issue == null) { + showErrorNotification("Error: No finding selected"); + return; + } + + // Resolve the project from the issue's file path (per-finding resolution) + IProject project = resolveProjectFromFilePath(issue.getFilePath()); + if (project == null) { + showErrorNotification("Error: Could not locate project for this finding"); + return; + } + + // Use IgnoreManager to add the issue (matches JetBrains implementation) + IgnoreManager ignoreManager = IgnoreManager.getInstance(project); + ignoreManager.addIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + TelemetryService.logIgnorePackageAction(issue); + + // Refresh the tree to remove the ignored finding from the view + refreshTreeWithFilter(); + com.checkmarx.eclipse.devassist.ui.findings.ignore.DevAssistIgnoredFindings.refreshIfOpen(); + + CxLogger.info("Successfully ignored finding: " + issue.getTitle()); + + } 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 + * Matches JetBrains implementation: addAllIgnoredEntry() + */ + private void ignoreAllOfType(ScanIssue issue) { + try { + if (issue == null) { + showErrorNotification("Error: No finding selected"); + return; + } + + // Resolve the project from the issue's file path (per-finding resolution) + IProject project = resolveProjectFromFilePath(issue.getFilePath()); + if (project == null) { + showErrorNotification("Error: Could not locate project for this finding"); + return; + } + + // Use IgnoreManager to add all matching issues (matches JetBrains implementation) + IgnoreManager ignoreManager = IgnoreManager.getInstance(project); + ignoreManager.addAllIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + TelemetryService.logIgnoreAllAction(issue); + + // Refresh the tree to remove the ignored findings from the view + refreshTreeWithFilter(); + com.checkmarx.eclipse.devassist.ui.findings.ignore.DevAssistIgnoredFindings.refreshIfOpen(); + + CxLogger.info("Successfully ignored all findings of type: " + issue.getTitle()); + + } 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 { + // Delegate registration/dedup to the single, shared CheckmarxEditorListener + // (registered once at plugin startup) instead of creating a second, + // independent CheckmarxDocumentListener/RealTimeScanJob pair here. Two + // independent pairs attached to the same document each debounce and + // reschedule scans on their own, colliding on the shared per-file + // in-flight lock in DevAssistScanStateHolder - an edit's scan can get + // silently BLOCKED by an unrelated duplicate job that is still in-flight, + // with nothing left to reschedule it, making scans appear to trigger only + // after a further edit. + CheckmarxEditorListener sharedListener = CheckmarxEditorListener.getInstance(); + org.eclipse.jface.text.IDocument document = null; + if (sharedListener != null) { + sharedListener.ensureRealtimeScanningForEditor(editor); + } else { + // Fallback: shared listener not initialized yet (should not happen once + // PluginStartup has run) - register a listener directly so scanning still + // works. + document = getDocumentFromEditorFallback(editor); + if (document != null) { + String fileName = file.getName(); + RealTimeScanJob scanJob = new RealTimeScanJob(file, fileName); + CheckmarxDocumentListener docListener = new CheckmarxDocumentListener(fileName, scanJob, file, null); + document.addDocumentListener(docListener); + } + } + + // Apply cached decorations if findings exist for this file + // Pass the editor directly to avoid search issues with MavenPomEditor + if (document == null) { + document = getDocumentFromEditorFallback(editor); + } + if (document != null) { + 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(); + } + } + + /** + * Extract the IDocument from an editor, trying the same fallback chain as + * CheckmarxEditorListener (direct ITextEditor, adapter, IDocument adapter) - + * needed here only to feed applyCachedDecorationsForFile(), which requires a + * document even though listener registration itself is now delegated. + */ + private org.eclipse.jface.text.IDocument getDocumentFromEditorFallback(IEditorPart editor) { + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + try { + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider() + .getDocument(textEditor.getEditorInput()); + if (doc != null) { + return doc; + } + } catch (Exception e) { + // fall through + } + } + org.eclipse.ui.texteditor.ITextEditor textEditor = editor + .getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor != null) { + try { + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider() + .getDocument(textEditor.getEditorInput()); + if (doc != null) { + return doc; + } + } catch (Exception e) { + // fall through + } + } + return editor.getAdapter(org.eclipse.jface.text.IDocument.class); + } + + /** + * Apply cached decorations (gutter icons, underlines) when editor is opened via + * navigation. + * + * Delegates to ProblemDecorator.decorateEditor() (the same entry point used by + * refreshTreeWithFilter()) rather than duplicating annotation-creation logic, + * so this path also gets marker creation, stale-annotation clearing, and the + * "ignored" gutter-icon pass for any .checkmarxIgnored entries on this file - + * matching the JetBrains plugin's restoreGutterIcons()-on-file-open behavior. + */ + 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<ScanIssue> cachedIssues = problemHolder.getScanIssuesByFile(filePath); + + // Filter to only exclude ignored issues - include OK/UNKNOWN gutter icons + // This matches the refreshTreeWithFilter() approach: pass all non-ignored + // issues to ProblemDecorator for proper gutter icon decoration + IgnoreManager ignoreManager = IgnoreManager.getInstance(project); + java.util.List<ScanIssue> activeIssues = new java.util.ArrayList<>(); + for (ScanIssue issue : cachedIssues) { + if (issue == null || issue.getSeverity() == null) { + continue; + } + // Only filter out ignored - keep OK/UNKNOWN for gutter icons + if (ignoreManager.isIgnored(issue)) { + continue; + } + activeIssues.add(issue); + } + + com.checkmarx.eclipse.devassist.problems.ProblemDecorator.decorateEditor(file, activeIssues); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] Error applying cached decorations: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 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 a marker exists for this issue. Markers are now created eagerly for + * every + * detected finding as soon as it's decorated (see + * ProblemDecorator.decorateEditor(), which + * calls MarkerIssueMapper.ensureMarker() for each issue) - this remains as a + * safety net for + * the navigate-to-finding flow in case the file wasn't open (and therefore + * wasn't decorated) + * when the finding was first reported. + */ + private void createMarkerForIssue(IFile file, ScanIssue issue) { + com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.ensureMarker(file, issue); + } + + /** + * 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 = com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.findMarker(file, + issue); + if (marker != null && marker.exists()) { + org.eclipse.ui.ide.IDE.gotoMarker(editor, marker); + + } else { + + } + } catch (Exception e) { + + } + } + + 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: Fix with AI Assist + org.eclipse.swt.widgets.MenuItem fixWithAIItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + fixWithAIItem.setImage(STAR_ICON); + fixWithAIItem.setText(DevAssistConstants.FIX_WITH_CXONE_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); + TelemetryService.logFixWithCxOneAssistAction(issue); + } + }); + + // Menu Item 2: View Details + org.eclipse.swt.widgets.MenuItem viewDetailsItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + viewDetailsItem.setImage(STAR_ICON); + viewDetailsItem.setText(DevAssistConstants.VIEW_DETAILS_FIX_NAME); + 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); + TelemetryService.logViewDetailsAction(issue); + } + }); + + // Menu Item 3: Ignore This Finding + org.eclipse.swt.widgets.MenuItem ignoreItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + ignoreItem.setImage(STAR_ICON); + ignoreItem.setText(DevAssistConstants.IGNORE_THIS_VULNERABILITY_FIX_NAME); + 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.setImage(STAR_ICON); + ignoreAllItem.setText(DevAssistConstants.IGNORE_ALL_OF_THIS_TYPE_FIX_NAME); + 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); + } + + public void refreshTreeWithFilter() { + + // Apply active filters and refresh + VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); + + Map<String, List<ScanIssue>> filteredIssues = new HashMap<>(); + Map<String, List<ScanIssue>> decorationIssues = new HashMap<>(); + int totalAfter = 0; + + for (String filePath : currentIssues.keySet()) { + List<ScanIssue> issues = currentIssues.get(filePath); + if (issues == null) + continue; + + // Resolve the project for this file path (per-file resolution, not stale currentProject) + IProject project = resolveProjectFromFilePath(filePath); + + List<ScanIssue> filtered = new java.util.ArrayList<>(); + List<ScanIssue> forDecoration = 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; + } + + boolean isIgnored = project != null + && IgnoreManager.getInstance(project).isIgnored(issue); + boolean hasFilter = filterState.hasFilter(issue.getSeverity()); + boolean isProblem = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.isProblem(issue.getSeverity()); + + // For decoration: include OK/UNKNOWN gutter icons (only exclude ignored) + if (!isIgnored) { + forDecoration.add(issue); + } + + // Filter by OK/UNKNOWN/IGNORED severity (Phase 3) - for Problems tree only + 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(); + } + + if (!forDecoration.isEmpty()) { + decorationIssues.put(filePath, forDecoration); + } + } + + // ✅ 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<Object> validExpanded = new java.util.ArrayList<>(); + for (Object element : expandedElements) { + if (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (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 + if (totalAfter > 0) { + setPartName(DevAssistConstants.DEVASSIST_TAB+ " " + totalAfter); + } else { + setPartName(DevAssistConstants.DEVASSIST_TAB); + } + + // Apply decorations to open editors with issues that include OK/UNKNOWN gutter icons + // but exclude ignored findings. This ensures: + // - Problems tree shows only actual problems (no OK/UNKNOWN) + // - Gutter icons appear for OK/UNKNOWN findings in the editor + // - Ignored findings have no gutter icon or severity underline + applyDecorationsToOpenEditors(decorationIssues); + } + + /** + * Apply decorations to open editors for files whose active-finding set needs + * to change: files that currently have active findings, plus files that had + * active findings as of the previous pass but have none now (e.g. every + * finding in the file was just ignored). + * + * Deliberately does NOT iterate every file in {@code currentIssues} (which can + * include every file ever scanned this session, most with nothing to do) - + * every extra call here is a real decorateEditor() invocation that clears and + * rebuilds that file's annotations, so visiting files with no actual state + * change is not just wasted work but, combined with this plugin's existing + * scan-result pipeline firing more than one update per scan, widens the + * window for a stale/racing call to clobber a fresher one's annotations + * ("flicker and lose results"). Restricting the set to only files whose + * active-finding membership actually changed keeps the original + * fully-ignored-file fix while not manufacturing extra no-op calls. + * + * Also ensures annotations are present in the annotation model when the + * Findings View displays cached results, so hover can find them without + * waiting for a new scan. + */ + private void applyDecorationsToOpenEditors(Map<String, List<ScanIssue>> filteredIssues) { + Map<String, List<ScanIssue>> effectiveFiltered = filteredIssues != null + ? filteredIssues + : java.util.Collections.emptyMap(); + + if (effectiveFiltered.isEmpty() && lastDecoratedFiles.isEmpty()) { + return; + } + + try { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page == null) { + return; + } + + Set<String> filesToDecorate = new HashSet<>(effectiveFiltered.keySet()); + filesToDecorate.addAll(lastDecoratedFiles); + + for (String filePath : filesToDecorate) { + List<ScanIssue> issues = effectiveFiltered.getOrDefault(filePath, java.util.Collections.emptyList()); + + // Find if this file is currently open in an editor + try { + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); + if (file != null && file.exists()) { + // Trigger decoration for this file's open editor (if any) + ProblemDecorator.decorateEditor(file, issues); + + // Ensure workspace problem markers exist for each issue so the Problems + // view shows the findings with the correct severity mapping. + try { + for (ScanIssue issue : issues) { + if (issue == null) + continue; + // createMarkerForIssue will delegate to MarkerIssueMapper.ensureMarker + // which maps Checkmarx severities to Eclipse marker severities + createMarkerForIssue(file, issue); + } + } catch (Exception e) { + // Marker creation is best-effort; log and continue decorating other files + System.err.println("[FINDINGS] Error ensuring markers for file " + file.getLocation() + ": " + e.getMessage()); + } + } + } catch (Exception e) { + // Log but continue with other files + System.err.println("[FINDINGS] Error decorating file " + filePath + ": " + e.getMessage()); + } + } + + lastDecoratedFiles = new HashSet<>(effectiveFiltered.keySet()); + } catch (Exception e) { + System.err.println("[FINDINGS] Error applying decorations to open editors: " + e.getMessage()); + } + } + + /** + * 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<String, List<ScanIssue>> issues) { + if (issues == null) + return; + // Log issues by severity + Map<String, Long> 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); + } + }); + } + }); + + 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; + } + +} 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..c33b4aaf --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java @@ -0,0 +1,115 @@ +package com.checkmarx.eclipse.devassist.ui.findings.actions; + +import org.eclipse.jface.action.Action; + +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * Base toggle action for severity filters in the Findings view. + */ +public abstract class VulnerabilityFilterAction extends Action { + + private final String severity; + private final IFilterChangeListener filterChangeListener; + private final VulnerabilityFilterState filterState; + + public interface IFilterChangeListener { + void onFilterChanged(); + } + + public VulnerabilityFilterAction(String severity, IFilterChangeListener listener) { + this(severity, listener, VulnerabilityFilterState.getInstance()); + } + + public VulnerabilityFilterAction(String severity, IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super(severity, Action.AS_CHECK_BOX); + this.severity = severity; + this.filterChangeListener = listener; + this.filterState = filterState; + + // Icon-only toolbar actions to prevent wrapping - use tooltip for label + // Toolbar already shows the full text in other views, so we just use icons here + setText(""); // Empty text to keep toolbar compact + + // Load icon with theme-aware variant + String iconSuffix = DevAssistUtils.isDarkTheme() ? "_20_dark.svg" : "_20.svg"; + setImageDescriptor(org.eclipse.ui.plugin.AbstractUIPlugin + .imageDescriptorFromPlugin("com.checkmarx.eclipse.devassist", "icons/" + severity + iconSuffix)); + + // Apply theme-aware tooltip - provides the full label when hovering + String label = severity.substring(0, 1).toUpperCase() + severity.substring(1); + String tooltip = "Filter " + label + " severity findings"; + if (DevAssistUtils.isDarkTheme()) { + tooltip += " (Light selection highlight in dark theme)"; + } else { + tooltip += " (Gray selection highlight in light theme)"; + } + setToolTipText(tooltip); + + // Set initial state + setChecked(filterState.hasFilter(severity)); + } + + @Override + public void run() { + 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 MaliciousFilter(IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super("malicious", listener, filterState); + } + } + + public static class CriticalFilter extends VulnerabilityFilterAction { + public CriticalFilter(IFilterChangeListener listener) { + super("critical", listener); + } + public CriticalFilter(IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super("critical", listener, filterState); + } + } + + public static class HighFilter extends VulnerabilityFilterAction { + public HighFilter(IFilterChangeListener listener) { + super("high", listener); + } + public HighFilter(IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super("high", listener, filterState); + } + } + + public static class MediumFilter extends VulnerabilityFilterAction { + public MediumFilter(IFilterChangeListener listener) { + super("medium", listener); + } + public MediumFilter(IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super("medium", listener, filterState); + } + } + + public static class LowFilter extends VulnerabilityFilterAction { + public LowFilter(IFilterChangeListener listener) { + super("low", listener); + } + public LowFilter(IFilterChangeListener listener, VulnerabilityFilterState filterState) { + super("low", listener, filterState); + } + } +} 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..8c32ca33 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java @@ -0,0 +1,71 @@ +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 a findings view. + * + * {@link #getInstance()} exposes the shared singleton used by the main + * Findings view. Other views (e.g. the Ignored Findings window) that want + * their own, independent severity filter - not one that toggles in lockstep + * with the main view - can instead construct their own instance directly. + */ +public class VulnerabilityFilterState { + + private static final VulnerabilityFilterState INSTANCE = new VulnerabilityFilterState(); + + private final Set<String> selectedFilters = Collections.synchronizedSet(new HashSet<>()); + + public 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<String> 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 = "<table style='display:inline-table;vertical-align:middle;border-collapse:collapse;'><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("<html><body style='margin:0;padding:0;'>"); + + 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("</body></html>"); + return descBuilder.toString(); + } + + private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("<td style='padding:0 6px 0 0;vertical-align:middle;'></td>") + .append("<td style='padding:0 2px 0 2px;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<p style='margin:0;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append("<b>").append(escapeHtml(scanIssue.getTitle())).append("@") + .append(escapeHtml(scanIssue.getPackageVersion())).append("</b>") + .append(" - <span style='").append(SECONDARY_SPAN_STYLE).append("'>") + .append(escapeHtml(scanIssue.getSeverity())).append(" Risk Package") + .append("</span></p></td></tr></table>"); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + private void buildContainerDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("<td style='padding:0 6px 0 0;vertical-align:middle;'></td>") + .append("<td style='padding:0 2px 0 2px;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<p style='margin:0;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append("<b>").append(escapeHtml(scanIssue.getTitle())).append("@") + .append(escapeHtml(scanIssue.getImageTag())).append("</b>") + .append("</p></td></tr></table>"); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + private void buildIACDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + List<Vulnerability> vulnerabilities = scanIssue.getVulnerabilities(); + if (vulnerabilities != null) { + for (Vulnerability vulnerability : vulnerabilities) { + descBuilder.append(TABLE_WITH_TR) + .append("<td style='width:20px;padding:0 6px 0 0;vertical-align:middle;'></td>") + .append("<td style='padding:0 6px 0 6px;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY) + .append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<p style='").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append("<b>").append(escapeHtml(vulnerability.getTitle())).append("</b>") + .append(" - ").append(escapeHtml(vulnerability.getDescription())) + .append(" - <span style='").append(SECONDARY_SPAN_STYLE).append("'>IaC vulnerability</span>") + .append("</p></td></tr></table>"); + } + } + } + + private void buildSecretsDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("<td style='padding:0 6px 0 0;vertical-align:middle;'></td>") + .append("<td style='padding:0 2px 0 2px;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<p style='margin:0;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append("<b>").append(escapeHtml(formatTitle(scanIssue.getTitle()))).append("</b>") + .append(" - <span style='").append(SECONDARY_SPAN_STYLE).append("'>Secret finding</span>") + .append("</p></td></tr></table>"); + } + + private void buildASCADescription(StringBuilder descBuilder, ScanIssue scanIssue) { + List<Vulnerability> vulnerabilities = scanIssue.getVulnerabilities(); + if (vulnerabilities != null) { + for (Vulnerability vulnerability : vulnerabilities) { + descBuilder.append(TABLE_WITH_TR) + .append("<td style='width:20px;padding:0 6px 0 0;vertical-align:middle;'></td>") + .append("<td style='padding:0 6px 0 6px;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY) + .append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<p style='").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append("<b>").append(escapeHtml(vulnerability.getTitle())).append("</b>") + .append(" - ").append(escapeHtml(vulnerability.getDescription())) + .append(" - <span style='").append(SECONDARY_SPAN_STYLE).append("'>SAST vulnerability</span>") + .append("</p></td></tr></table>"); + } + } + } + + private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append("<div><b>").append(escapeHtml(scanIssue.getTitle())).append("</b> -") + .append(escapeHtml(scanIssue.getDescription())).append("</div>"); + } + + private void buildVulnerabilitySection(StringBuilder descBuilder, ScanIssue scanIssue) { + List<Vulnerability> vulnerabilityList = scanIssue.getVulnerabilities(); + if (vulnerabilityList == null || vulnerabilityList.isEmpty()) { + return; + } + + descBuilder.append("<div>").append(TABLE_WITH_TR); + Map<String, Long> vulnerabilityCount = vulnerabilityList.stream() + .map(Vulnerability::getSeverity) + .collect(Collectors.groupingBy(severity -> severity, Collectors.counting())); + + vulnerabilityCount.forEach((severity, count) -> { + descBuilder.append("<td style='padding:0;'></td>") + .append("<td style='font-size:9px;color:#ADADAD;vertical-align:middle;padding:0 4px 0 1px;'>") + .append(count).append("</td>"); + }); + + descBuilder.append("</tr></table></div>"); + } + + /** + * 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..e0cf85e8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java @@ -0,0 +1,57 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.jface.text.source.Annotation; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +public class FindingsAnnotation extends Annotation { + + private String title; + private String description; + private ScanIssue scanIssue; + private List<AnnotationButton> buttons = new ArrayList<>(); + + public FindingsAnnotation(String type, String title, String description) { + super(type, false, null); + this.title = title; + this.description = description; + } + + public FindingsAnnotation(String type, String title, String description, ScanIssue scanIssue) { + super(type, false, null); + this.title = title; + this.description = description; + this.scanIssue = scanIssue; + } + + public void addButton(String label, Runnable action) { + buttons.add(new AnnotationButton(label, action)); + } + + public List<AnnotationButton> getButtons() { + return buttons; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public ScanIssue getScanIssue() { + return scanIssue; + } + + 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/hover/CheckmarxAnnotationHover.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxAnnotationHover.java new file mode 100644 index 00000000..c4f22601 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxAnnotationHover.java @@ -0,0 +1,773 @@ +package com.checkmarx.eclipse.devassist.ui.findings.hover; + +import java.awt.Robot; +import java.awt.event.InputEvent; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.internal.text.html.BrowserInformationControl; +import org.eclipse.jface.text.AbstractReusableInformationControlCreator; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DefaultInformationControl; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IInformationControl; +import org.eclipse.jface.text.IInformationControlCreator; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextHoverExtension; +import org.eclipse.jface.text.ITextHoverExtension2; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelExtension2; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.browser.ProgressEvent; +import org.eclipse.swt.browser.ProgressListener; + +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.remediation.RemediationLinkHandler; +import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.HtmlEscapeUtil; +import org.eclipse.jface.resource.JFaceColors; + +/** + * Line hover for Checkmarx findings, contributed to the JDT Java editor via + * org.eclipse.jdt.ui.javaEditorTextHovers (the only public Eclipse extension + * point for adding a hover to an editor this plugin does not own). Renders the + * Checkmarx problem description as HTML, and appends the text of any OTHER + * annotations already present on the same line (JDT compiler errors, other + * linters, etc.) so hovering never hides existing information for the line - it + * only adds to it. This mirrors how JetBrains merges multiple inspection + * results (HighlightInfo entries) into a single hover popup. + * <p> + * Must implement IJavaEditorTextHover (not just ITextHover) because JDT's hover + * framework (JavaEditorTextHoverDescriptor.createTextHover()) casts contributed + * hover classes to IJavaEditorTextHover. + * <p> + * NOTE: JDT gates which contributed hovers are actually active via user + * preferences (Preferences > Java > Editor > Hovers, keyed by this hover's id + * and a modifier-key/state-mask). Registering the extension makes this hover + * available and selectable, but does NOT enable it by default - on a fresh + * install the user must check "Checkmarx Finding" in that preference page (and + * give it the "None"/combination slot to see it on a plain mouse hover with no + * modifier key). Until then, hovering shows Eclipse's default combination + * annotation hover instead (plain, unstyled marker text). + */ +public class CheckmarxAnnotationHover implements IJavaEditorTextHover, ITextHoverExtension2, ITextHoverExtension { + + /** + * Hover popup sizing is now automatic - content determines the size. + * The popup will automatically expand to fit the content and allows + * user resizing via dragging, consistent with non-Java editor hovers. + */ + + private static final CheckmarxProblemDescriptionFormatter PROBLEM_DESCRIPTRO = new CheckmarxProblemDescriptionFormatter(); + + /** + * Gates the verbose CxLogger.info/warning tracing inside getHoverInfo2() and + * buildCheckmarxSection() - both run on every mouse-hover tick, so + * unconditional log writes there add synchronous latency directly visible as + * popup lag. Off by default; enable with -Dcx.devassist.hover.debug=true + * for troubleshooting. Genuine error logging (CxLogger.error) is never + * gated. + */ + private static final boolean HOVER_DEBUG_LOGGING = Boolean.getBoolean("cx.devassist.hover.debug"); + + /** + * Creates the small (~6-line) preview control shown on the initial mouse hover. + * Mirrors JDT's own AbstractAnnotationHover/JavadocHover: the browser control + * returned here overrides getInformationPresenterControlCreator() to point at + * the enlarged PresenterControlCreator below - without that override, + * AbstractInformationControlManager.canReplace() always returns false, so + * moving the mouse toward the popup can never "enrich" it into the bigger, + * reachable control and the popup instead closes on the next pixel of mouse + * movement outside the hovered line. + * <p> + * Must extend AbstractReusableInformationControlCreator (not a bare + * IInformationControlCreator lambda/anonymous class) so the SAME browser widget + * is reused across repeated hover computations - otherwise + * AbstractInformationControlManager.getInformationControl() disposes and + * recreates the control on every mouse-hover tick (it only skips that when the + * creator implements IInformationControlCreatorExtension, which the reusable + * base class does), which was cutting the browser off mid-render before it + * could finish laying out the HTML. + */ + private final class HoverControlCreator extends AbstractReusableInformationControlCreator { + private final IInformationControlCreator presenterControlCreator; + + HoverControlCreator(IInformationControlCreator presenterControlCreator) { + this.presenterControlCreator = presenterControlCreator; + } + + @Override + public IInformationControl doCreateInformationControl(Shell parent) { + String tooltipAffordance = EditorsUI.getTooltipAffordanceString(); + if (BrowserInformationControl.isAvailable(parent)) { + BrowserInformationControl control = new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, + tooltipAffordance) { + @Override + public IInformationControlCreator getInformationPresenterControlCreator() { + return presenterControlCreator; + } + + @Override + public void setSizeConstraints(int maxWidth, int maxHeight) { + // Use default sizing - content determines popup size, user can resize by dragging + super.setSizeConstraints(maxWidth, maxHeight); + } + }; + control.setBackgroundColor( + JFaceColors.getInformationViewerBackgroundColor(parent.getDisplay())); + + control.setForegroundColor( + JFaceColors.getInformationViewerForegroundColor(parent.getDisplay())); + setupActionHandler(control); + return control; + } + return new DefaultInformationControl(parent, tooltipAffordance) { + @Override + public IInformationControlCreator getInformationPresenterControlCreator() { + return presenterControlCreator; + } + }; + } + + private void setupActionHandler(BrowserInformationControl control) { + try { + java.lang.reflect.Field browserField = BrowserInformationControl.class.getDeclaredField("fBrowser"); + browserField.setAccessible(true); + org.eclipse.swt.browser.Browser browser = (org.eclipse.swt.browser.Browser) browserField.get(control); + if (browser != null && !browser.isDisposed()) { + CxLogger.info("[HOVER] HoverControlCreator: Setting up LocationListener for action buttons"); + CheckmarxAnnotationHover.this.activeControl = control; + final String[] lastHandledLocation = new String[1]; + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changing: " + event.location); + dispatchCxOneAssistAction(event, lastHandledLocation); + } + + @Override + public void changed(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changed: " + event.location); + dispatchCxOneAssistAction(event, lastHandledLocation); + } + }); + browser.addProgressListener(new ProgressListener() { + @Override + public void changed(ProgressEvent event) { + // no-op: only the final completed() matters here + } + + @Override + public void completed(ProgressEvent event) { + // Content is now laid out - popup size is determined by content + // No fixed size constraints applied + } + }); + CxLogger.info("[HOVER] LocationListener added successfully to HoverControlCreator"); + } else { + CxLogger.info("[HOVER] HoverControlCreator: Browser is null or disposed"); + } + } catch (Exception e) { + CxLogger.error("Failed to setup action handler for hover buttons (HoverControlCreator)", e); + } + } + } + + /** + * Shared dispatch logic for both LocationListener callbacks below. SWT's + * Browser widget does not reliably fire the same callback for a + * "#cxonedevassist/" link click across platforms/browser engines - observed: + * on one macOS run changing() fired with the action URL and changed() never + * did; on another macOS run changing() never fired for the click at all and + * only changed() did. Since which callback fires isn't dependable, both + * changing() and changed() call this, and lastHandledLocation[0] dedupes the + * case where both end up firing for the same click. + */ + private void dispatchCxOneAssistAction(LocationEvent event, String[] lastHandledLocation) { + CxLogger.info("[HOVER] Dispatch CxOneAssist Action called: " + event.location); + int actionIndex = event.location.indexOf("#cxonedevassist/"); + if (actionIndex < 0) { + CxLogger.info("[HOVER] Dispatch CxOneAssist Action called: actionIndex " + actionIndex); + return; + } + event.doit = false; + if (event.location.equals(lastHandledLocation[0])) { + CxLogger.info("[HOVER] Dispatch CxOneAssist Action called: event location equals lastHandledLocation " + event.location.equals(lastHandledLocation[0])); + return; + } + lastHandledLocation[0] = event.location; + CxLogger.info("[HOVER] Blocking remediation action URL: " + event.location); + String linkData = event.location.substring(actionIndex + 16); // +16 for "#cxonedevassist/" + CxLogger.info("[HOVER] Extracted link data: " + linkData); + handleHoverAction(linkData); + } + + private void handleHoverAction(String action) { + CxLogger.info("[HOVER] Action button clicked: " + action); + + if (currentFinding == null) { + CxLogger.info("[HOVER] No finding context available for action: " + action); + return; + } + + RemediationLinkHandler linkHandler = new RemediationLinkHandler(); + boolean handled = linkHandler.handleLink(action, currentFinding); + + if (!handled) { + CxLogger.info("[HOVER] Unknown or unhandled action: " + action); + } + + // Auto-close hover after any button action (copyfixprompt, viewdetails, ignore*, etc.) + // to reset deduplication state and allow re-clicking the same action from a fresh popup + if (handled) { + closeHover(); + } + } + + private void closeHover() { + Display.getDefault().asyncExec(() -> { + try { + if (activeControl != null) { + activeControl.setVisible(false); + activeControl.dispose(); + activeControl = null; + } + Shell activeShell = Display.getDefault().getActiveShell(); + if (activeShell != null && !activeShell.isDisposed()) { + org.eclipse.swt.widgets.Event event = new org.eclipse.swt.widgets.Event(); + event.type = SWT.KeyDown; + event.keyCode = SWT.ESC; + event.character = SWT.ESC; + activeShell.traverse(SWT.TRAVERSE_ESCAPE, event); + activeShell.notifyListeners(SWT.KeyDown, event); + } + } catch (Exception e) { + CxLogger.error("[HOVER] Exception while closing hover: " + e.getMessage(), e); + } + }); + } + + /** + * Creates the enlarged, resizable, focusable control that replaces the small + * preview once the mouse moves toward it - this is what actually lets the user + * read the full finding and reach the action links. + */ + private final class PresenterControlCreator extends AbstractReusableInformationControlCreator { + @Override + public IInformationControl doCreateInformationControl(Shell parent) { + if (BrowserInformationControl.isAvailable(parent)) { + BrowserInformationControl control = new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, + true) { + + @Override + public void setSizeConstraints(int maxWidth, int maxHeight) { + // Use default sizing - content determines popup size, user can resize by dragging + super.setSizeConstraints(maxWidth, maxHeight); + } + }; + control.setBackgroundColor( + JFaceColors.getInformationViewerBackgroundColor(parent.getDisplay())); + + control.setForegroundColor( + JFaceColors.getInformationViewerForegroundColor(parent.getDisplay())); + setupActionHandler(control); + return control; + } + return new DefaultInformationControl(parent, true); + } + + private void setupActionHandler(BrowserInformationControl control) { + try { + java.lang.reflect.Field browserField = BrowserInformationControl.class.getDeclaredField("fBrowser"); + browserField.setAccessible(true); + org.eclipse.swt.browser.Browser browser = (org.eclipse.swt.browser.Browser) browserField.get(control); + if (browser != null && !browser.isDisposed()) { + CxLogger.info("[HOVER] PresenterControlCreator: Setting up LocationListener for action buttons"); + CheckmarxAnnotationHover.this.activeControl = control; + final String[] lastHandledLocation = new String[1]; + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changing: " + event.location); + dispatchCxOneAssistAction(event, lastHandledLocation); + } + + @Override + public void changed(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changed: " + event.location); + dispatchCxOneAssistAction(event, lastHandledLocation); + } + }); + browser.addProgressListener(new ProgressListener() { + @Override + public void changed(ProgressEvent event) { + // no-op: only the final completed() matters here + } + + @Override + public void completed(ProgressEvent event) { + // Content is now laid out - popup size is determined by content + // No fixed size constraints applied + } + }); + CxLogger.info("[HOVER] LocationListener added successfully to PresenterControlCreator"); + } else { + CxLogger.info("[HOVER] PresenterControlCreator: Browser is null or disposed"); + } + } catch (Exception e) { + CxLogger.error("Failed to setup action handler for hover buttons (PresenterControlCreator)", e); + } + } + } + + private IInformationControlCreator hoverControlCreator; + private IInformationControlCreator presenterControlCreator; + // Instance-scoped (not static): CheckmarxAnnotationHover is registered under + // two independent Eclipse extension points (javaEditorTextHovers and + // genericeditor.hoverProviders - see plugin.xml), so at least two live + // instances exist once a Java file and a non-Java file are open + // simultaneously, and JDT/the generic editor framework may also create + // further per-editor instances. A static field here would let hovering in + // one open editor overwrite the finding backing a still-open/interactive + // popup in another editor, causing a click on the stale popup's link to + // fire the wrong finding's action. Scoping it per-instance ties the field + // to the single hover popup lifecycle (setEditor()/getHoverInfo2()/ + // handleHoverAction() on the same object) that actually owns it. + private ScanIssue currentFinding; + // Stores reference to the active information control so it can be closed + // when ignore/ignore-all actions complete + private IInformationControl activeControl; + + @Override + public void setEditor(IEditorPart editor) { + // No editor-specific state needed: getHoverInfo2() derives everything + // it needs from the ITextViewer/ISourceViewer passed at hover time. + } + + /** + * Returns the whole line as the hover's "subject area" rather than a zero-width + * point at the cursor. JFace keeps the popup alive only while the mouse stays + * inside this region, so a zero-width region gave the mouse nowhere to go - it + * dismissed on the next pixel of movement, before the browser control could + * finish laying out the full HTML and before the mouse could travel toward the + * popup to interact with it. + */ + @Override + public IRegion getHoverRegion(ITextViewer textViewer, int offset) { + IDocument document = textViewer.getDocument(); + if (document != null) { + try { + return document.getLineInformationOfOffset(offset); + } catch (BadLocationException e) { + // fall through to point region below + } + } + return new Region(offset, 0); + } + + /** + * Without this, JFace falls back to a plain-text control and the HTML markup + * produced by getHoverInfo2()/CheckmarxProblemDescriptionFormatter would either + * show as literal tags or be flattened to plain text - the same + * BrowserInformationControl mechanism JDT's own Javadoc/Problem hovers use to + * render rich HTML. + * <p> + * Returns a cached instance (not a fresh one per call) because + * TextViewerHoverManager.computeInformation() calls this on every hover + * computation and re-registers whatever it gets via + * setCustomInformationControlCreator() - a stable, + * AbstractReusableInformationControlCreator-based instance lets that call + * recognize "same creator" and keep reusing the existing control instead of + * tearing it down and rebuilding it each time. + */ + @Override + public IInformationControlCreator getHoverControlCreator() { + if (hoverControlCreator == null) { + hoverControlCreator = new HoverControlCreator(getPresenterControlCreator()); + } + return hoverControlCreator; + } + + private IInformationControlCreator getPresenterControlCreator() { + if (presenterControlCreator == null) { + presenterControlCreator = new PresenterControlCreator(); + } + return presenterControlCreator; + } + + @Override + public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { + Object info = getHoverInfo2(textViewer, hoverRegion); + return info != null ? info.toString() : null; + } + + @Override + public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) { + long startTime = System.currentTimeMillis(); + try { + if (!(textViewer instanceof ISourceViewer)) { + return null; + } + ISourceViewer sourceViewer = (ISourceViewer) textViewer; + IAnnotationModel model = sourceViewer.getAnnotationModel(); + IDocument document = sourceViewer.getDocument(); + if (model == null || document == null) { + return null; + } + + int lineNumber; + try { + lineNumber = document.getLineOfOffset(hoverRegion.getOffset()); + } catch (Exception e) { + CxLogger.error("CheckmarxAnnotationHover: failed to get line number", e); + return null; + } + + StringBuilder html = new StringBuilder(); +// html.append("<html><body style='margin:0;padding:4px;font-family:Arial,sans-serif;font-size:11px;") +// .append("word-wrap:break-word;overflow-wrap:break-word;'>"); + + // Single UI-thread round trip for all theme-dependent colors instead of + // three independent syncExec() calls (background/foreground/element text + // color) - this hover computation runs on every mouse-hover tick, so + // collapsing three sequential blocking round trips into one matters here. + HoverColors hoverColors = getHoverColors(); + String backgroundColor = hoverColors.backgroundHex; + String foregroundColor = hoverColors.foregroundHex; + + html.append("<html>") + .append("<head>") + .append("<style>") + .append("html, body {") + .append("background-color:").append(backgroundColor).append(";") + .append("color:").append(foregroundColor).append(";") + .append("}") + .append("</style>") + .append("</head>") + .append("<body style='") + .append("margin:0;") + .append("padding:4px;") + .append("font-family:Arial,sans-serif;") + .append("font-size:11px;") + .append("word-wrap:break-word;") + .append("overflow-wrap:break-word;'>"); + + // Determine text color for dynamic elements in the formatter + // (e.g., ASCA/IAC vulnerability titles). Already resolved as part of the + // single hoverColors lookup above, so it's safe to use from the formatter + // via parameter passing without another UI-thread round trip. + String textColorForElements = hoverColors.textColorForElements; + + Set<Long> seenMarkerIds = new HashSet<>(); + // Tracks scanIssueIds already rendered via a FindingsAnnotation (the live, + // fully-populated ScanIssue) so a MarkerAnnotation for the SAME issue - which + // Eclipse creates the moment a finding is clicked in the Findings view, and + // which coexists indefinitely alongside the FindingsAnnotation in the same + // annotation model - doesn't render the finding a second time with only its + // root title/description (MarkerIssueMapper's marker-attribute reconstruction + // is inherently lossier than the live object). + Set<String> renderedIssueIds = new HashSet<>(); + // Fallback dedup key for issues without scanIssueId (e.g., OSS): title+line. + // Mirrors MarkerIssueMapper.findMarker()'s line+title heuristic for issues + // without a stable scanIssueId. + Set<String> renderedIssueKeys = new HashSet<>(); + List<String> checkmarxSections = new ArrayList<>(); + List<String> otherMessages = new ArrayList<>(); + + // Scan only annotations overlapping the hovered line's region instead of + // walking every annotation in the document - for files with many findings, + // getAnnotationIterator() over the whole document is O(total document + // annotations) per hover tick. IAnnotationModelExtension2 lets us ask the + // model to pre-filter to the region of interest (with canStartBefore/ + // canEndAfter so multi-line annotations that merely overlap the hovered + // line, rather than starting/ending on it, are still included). + List<Annotation> lineAnnotations = new ArrayList<>(); + IRegion lineRegion; + try { + lineRegion = document.getLineInformation(lineNumber); + } catch (BadLocationException e) { + lineRegion = hoverRegion; + } + + Iterator<Annotation> it; + if (model instanceof IAnnotationModelExtension2) { + it = ((IAnnotationModelExtension2) model).getAnnotationIterator(lineRegion.getOffset(), + Math.max(lineRegion.getLength(), 1), true, true); + } else { + it = model.getAnnotationIterator(); + } + + while (it.hasNext()) { + Annotation annotation = it.next(); + if (annotation == null || annotation.isMarkedDeleted()) { + continue; + } + + Position position = null; + try { + position = model.getPosition(annotation); + } catch (Exception e) { + continue; + } + + if (position == null || !isOnLine(document, position, lineNumber)) { + continue; + } + + lineAnnotations.add(annotation); + } + + // Pass 1: FindingsAnnotation first - it carries the live ScanIssue (full + // vulnerabilities list intact), so it takes priority over any MarkerAnnotation + // reconstruction of the same underlying issue. + for (Annotation annotation : lineAnnotations) { + if (!(annotation instanceof FindingsAnnotation)) { + continue; + } + FindingsAnnotation findingsAnn = (FindingsAnnotation) annotation; + ScanIssue scanIssue = findingsAnn.getScanIssue(); + if (scanIssue == null) { + continue; + } + currentFinding = scanIssue; + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] Captured ScanIssue for action handlers: " + scanIssue.getTitle()); + } + + if (scanIssue.getScanIssueId() != null && !scanIssue.getScanIssueId().isEmpty()) { + renderedIssueIds.add(scanIssue.getScanIssueId()); + if (HOVER_DEBUG_LOGGING) { + CxLogger.warning("[HOVER] Pass 1 - Added to renderedIssueIds: " + scanIssue.getScanIssueId()); + } + } else { + // Fallback dedup key for issues without scanIssueId. + // Use enhanced key that includes engine-specific identifiers (e.g., package@version for OSS) + String enhancedKey = getEnhancedFallbackKey(scanIssue); + String fallbackKey = buildFallbackDedupKey(enhancedKey, lineNumber); + renderedIssueKeys.add(fallbackKey); + if (HOVER_DEBUG_LOGGING) { + CxLogger.warning("[HOVER] Pass 1 - Added to renderedIssueKeys: " + fallbackKey); + } + } + + // Use consolidated formatter for both ASCA/IAC (iterates vulnerabilities) + // and other engines (uses root ScanIssue attributes) + try { + String sectionHtml = PROBLEM_DESCRIPTRO.formatDescriptionHtml(scanIssue, true, textColorForElements); + if (!sectionHtml.isEmpty()) { + checkmarxSections.add("<div>" + sectionHtml + "</div>"); + if (HOVER_DEBUG_LOGGING) { + ScanEngine engine = scanIssue.getScanEngine(); + String engineName = (engine != null) ? engine.toString() : "UNKNOWN"; + CxLogger.info("[HOVER] " + engineName + ": Rendered ScanIssue via formatter - " + + scanIssue.getTitle()); + } + } + } catch (Exception e) { + CxLogger.error("[HOVER] Error formatting FindingsAnnotation: " + e.getMessage(), e); + } + } + +// // Collect other linter/annotation messages (JDT, etc.) that aren't handled +// // above +// String message = annotation.getText(); +// if (message != null && !message.isEmpty()) { +// otherMessages.add(message); +// } +// } + + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] Line " + (lineNumber + 1) + ": Found " + checkmarxSections.size() + + " Checkmarx section(s), " + otherMessages.size() + " other message(s)"); + } + + if (checkmarxSections.isEmpty()) { + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] No Checkmarx findings to display, returning null"); + } + return null; + } + + for (int i = 0; i < checkmarxSections.size(); i++) { + if (i > 0) { + html.append("<hr style='margin:4px 0;border:none;border-top:1px solid #ccc;'/>"); + } + html.append(checkmarxSections.get(i)); + } + + if (!otherMessages.isEmpty()) { + html.append("<hr style='margin:4px 0;border:none;border-top:1px solid #ccc;'/>"); + for (String message : otherMessages) { + html.append("<div style='color:#666;font-size:10px;'>").append(HtmlEscapeUtil.escape(message)) + .append("</div>"); + } + } + + html.append("</body></html>"); + return html.toString(); + } finally { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > 100) { + CxLogger.info("CheckmarxAnnotationHover.getHoverInfo2() took " + elapsed + "ms"); + } + } + } + + + private String buildCheckmarxSection(IMarker marker, Long markerId, String textColor) { + try { + ScanIssue issue = MarkerIssueMapper.fromMarker(marker); + if (issue == null) { + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] Marker " + markerId + ": Failed to extract ScanIssue from marker"); + } + return ""; + } + currentFinding = issue; + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] Captured ScanIssue for action handlers from marker: " + issue.getTitle()); + } + // Use consolidated formatter with clickable actions enabled (same as + // FindingsAnnotation path) + String html = "<div>" + PROBLEM_DESCRIPTRO.formatDescriptionHtml(issue, true, textColor) + "</div>"; + if (HOVER_DEBUG_LOGGING) { + CxLogger.info("[HOVER] Marker " + markerId + ": Built HTML section for issue: " + issue.getTitle()); + } + return html; + } catch (Exception e) { + CxLogger.error("CheckmarxAnnotationHover: failed to build hover content for marker " + markerId, e); + return ""; + } + } + + private boolean isCheckmarxMarker(IMarker marker) { + try { + return marker != null && marker.exists() + && marker.isSubtypeOf("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); + } catch (Exception e) { + return false; + } + } + + private boolean isOnLine(IDocument document, Position position, int lineNumber) { + try { + int startLine = document.getLineOfOffset(position.getOffset()); + int endLine = document.getLineOfOffset(position.getOffset() + Math.max(position.getLength() - 1, 0)); + return lineNumber >= startLine && lineNumber <= endLine; + } catch (Exception e) { + return false; + } + } + + /** + * Holds all theme-dependent colors needed to render a hover popup, resolved + * together via {@link #getHoverColors()} in a single UI-thread round trip. + */ + private static final class HoverColors { + final String backgroundHex; + final String foregroundHex; + final String textColorForElements; + + HoverColors(String backgroundHex, String foregroundHex, String textColorForElements) { + this.backgroundHex = backgroundHex; + this.foregroundHex = foregroundHex; + this.textColorForElements = textColorForElements; + } + } + + /** + * Resolves the hover background color, hover foreground color, and the text + * color used for dynamic formatter elements (ASCA/IAC vulnerability titles) + * in one combined syncExec() call instead of three independent ones. + * getHoverInfo2() runs on every mouse-hover tick, so collapsing three + * sequential blocking UI-thread round trips into a single one matters here. + */ + private static HoverColors getHoverColors() { + Display display = Display.getDefault(); + final HoverColors[] result = new HoverColors[1]; + + Runnable runnable = () -> { + boolean darkTheme = DevAssistUtils.isDarkTheme(); + + String backgroundHex; + if (darkTheme) { + backgroundHex = "#393B3D"; + } else { + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + backgroundHex = toHex(bg); + } + + Color fg = JFaceColors.getInformationViewerForegroundColor(display); + String foregroundHex = toHex(fg); + + String textColorForElements = darkTheme ? "#FFFFFF" : "#000000"; + + result[0] = new HoverColors(backgroundHex, foregroundHex, textColorForElements); + }; + + if (Display.getCurrent() == display) { + runnable.run(); + } else { + display.syncExec(runnable); + } + + return result[0]; + } + + private static String toHex(Color color) { + return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); + } + + private static String buildFallbackDedupKey(String title, int lineNumber) { + return (title != null ? title : "") + "|" + lineNumber; + } + + /** + * Builds an enhanced fallback key that uniquely identifies a scan issue + * when no stable scanIssueId is available. Incorporates engine-specific + * identifiers for better deduplication. + * + * @param scanIssue the scan issue to generate a key for + * @return a unique identifier string for the issue + */ + private static String getEnhancedFallbackKey(ScanIssue scanIssue) { + if (scanIssue == null || scanIssue.getTitle() == null) { + return ""; + } + + ScanEngine engine = scanIssue.getScanEngine(); + + // For OSS packages, include version for uniqueness + if (engine == ScanEngine.OSS) { + String version = scanIssue.getPackageVersion(); + if (version != null && !version.isEmpty()) { + return scanIssue.getTitle() + "@" + version; + } + } + + // Default: use title only + return scanIssue.getTitle(); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java new file mode 100644 index 00000000..27e6d945 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java @@ -0,0 +1,522 @@ +package com.checkmarx.eclipse.devassist.ui.findings.hover; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.FileLocator; +import java.net.URL; +import java.util.Arrays; +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +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.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.HtmlEscapeUtil; +import static com.checkmarx.eclipse.devassist.ui.findings.hover.CheckmarxProblemDescriptionFormatter.InlineStyle.*; +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.SEPERATOR; + +/** + * Formats a ScanIssue as an HTML fragment for display in the editor's line + * hover. Consolidates all HTML rendering for both marker-based and live + * FindingsAnnotation paths. + * <p> + * Supports two action link modes: - Clickable (enableClickableActions=true): + * renders <a href='#action:...'> for LocationListener interception - + * Informational (enableClickableActions=false): renders plain text with Ctrl+1 + * hint + * <p> + * For ASCA/IAC issues that group multiple vulnerabilities on the same line, + * renders one block per vulnerability instead of collapsing to root attributes. + */ +public final class CheckmarxProblemDescriptionFormatter { + + private static final Map<String, String> DESCRIPTION_ICON = new LinkedHashMap<>(); + + private static final String COUNT = "COUNT"; + private static final String PACKAGE = "Package"; + private static final String DEV_ASSIST = "DevAssist"; + private static final String CONTAINER = "Container"; + + public CheckmarxProblemDescriptionFormatter() { + initIconsMap(); + } + + private static void initIconsMap() { + DESCRIPTION_ICON.put(SeverityLevel.MALICIOUS.getSeverity(), + getImage(DevAssistConstants.ImagePaths.MALICIOUS_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.CRITICAL.getSeverity(), + getImage(DevAssistConstants.ImagePaths.CRITICAL_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.HIGH.getSeverity(), getImage(DevAssistConstants.ImagePaths.HIGH_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.MEDIUM.getSeverity(), getImage(DevAssistConstants.ImagePaths.MEDIUM_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.LOW.getSeverity(), getImage(DevAssistConstants.ImagePaths.LOW_PNG)); + + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.CRITICAL.getSeverity()), + getImage(DevAssistConstants.ImagePaths.CRITICAL_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.HIGH.getSeverity()), + getImage(DevAssistConstants.ImagePaths.HIGH_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.MEDIUM.getSeverity()), + getImage(DevAssistConstants.ImagePaths.MEDIUM_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.LOW.getSeverity()), + getImage(DevAssistConstants.ImagePaths.LOW_16_PNG)); + + DESCRIPTION_ICON.put(PACKAGE, getImage(DevAssistConstants.ImagePaths.PACKAGE_PNG)); + DESCRIPTION_ICON.put(DEV_ASSIST, getImage(DevAssistConstants.ImagePaths.DEV_ASSIST_PNG)); + DESCRIPTION_ICON.put(CONTAINER, getImage(DevAssistConstants.ImagePaths.CONTAINER_PNG)); + } + + /** + * Build the HTML body (without outer html/body tags) describing the issue, + * suitable for embedding inside a BrowserInformationControl or merging with + * other annotations' hover text on the same line. + * <p> + * Supports both clickable action links (for CheckmarxAnnotationHover's + * BrowserInformationControl) and informational-only links (for marker + * resolution fallback). + * + * @param issue the scan issue + * @param enableClickableActions if true, renders action links as #action:... + * for LocationListener interception; if false, + * renders as informational text with Ctrl+1 hint + * @param textColor text color in hex format (e.g., "#000000" for dark themes, + * "#FFFFFF" for light), or null to use inherited color + * @return HTML fragment + */ + public String formatDescriptionHtml(ScanIssue scanIssue, boolean enableClickableActions, String textColor) { + StringBuilder descBuilder = new StringBuilder(); + + // DevAssist image + descBuilder.append(TABLE_WITH_TR).append("<td style='vertical-align:middle;'>") + .append(DESCRIPTION_ICON.get(DEV_ASSIST)).append("</td></tr></table>"); + descBuilder.append("<hr style='margin:4px 0;border:none;border-top:none solid #ccc;'/>"); + + // For ASCA and IAC multiple violations + appendMultipleViolationsTitle(descBuilder, scanIssue, textColor); + + switch (scanIssue.getScanEngine()) { + case OSS: + buildOSSDescription(descBuilder, scanIssue, textColor); + break; + case ASCA: + buildASCADescription(descBuilder, scanIssue, textColor); + break; + case SECRETS: + buildSecretsDescription(descBuilder, scanIssue, textColor); + break; + case IAC: + buildIACDescription(descBuilder, scanIssue, textColor); + break; + case CONTAINERS: + buildContainerDescription(descBuilder, scanIssue, textColor); + break; + default: + buildDefaultDescription(descBuilder, scanIssue); + } + if (scanIssue.getScanEngine() != ScanEngine.IAC && scanIssue.getScanEngine() != ScanEngine.ASCA) { + buildRemediationActionsSection(descBuilder, scanIssue.getScanIssueId(), scanIssue.getScanEngine().name()); + } + return descBuilder.toString(); + } + + /** + * Builds the OSS description for the provided scan issue and appends it to the + * given StringBuilder. This method incorporates severity-specific formatting, + * including handling for malicious packages, and assembles the description with + * the package header and vulnerability details. + * + * @param descBuilder the StringBuilder to which the formatted OSS description + * will be appended + * @param scanIssue the ScanIssue object containing information about the + * scanned issue, including its severity, vulnerabilities, + * and related details + */ + private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + buildPackageMessage(descBuilder, scanIssue, textColor); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + /** + * Builds the package header section of a description for a scan issue and + * appends it to the provided StringBuilder. This method formats information + * about the scan issue's severity, title, and package version, and includes an + * associated image icon representing the issue. + * + * @param descBuilder the StringBuilder to which the formatted package header + * information will be appended + * @param scanIssue the ScanIssue object containing details about the issue + * such as severity, title, and package version + */ + private static void buildPackageMessage(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String secondaryText = DevAssistConstants.SEVERITY_PACKAGE; + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + String iconKey = PACKAGE; + if (scanIssue.getSeverity().equalsIgnoreCase(SeverityLevel.MALICIOUS.getSeverity())) { + secondaryText = PACKAGE; + iconKey = scanIssue.getSeverity(); + } + String icon = getSeverityIconHtml(iconKey, ICON_INLINE_STYLE); + + descBuilder.append(TABLE_WITH_TR).append("<td style='padding:0 6px 0 0;vertical-align:middle;'>").append(icon) + .append("</td>").append("<td style=' " + colorStyle + " padding:0 2px 0 2px;").append(TITLE_FONT_SIZE) + .append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>").append("<p style='margin:0;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>").append("<b>") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append("@") + .append(HtmlEscapeUtil.escape(scanIssue.getPackageVersion())).append("</b>").append(" - <span style='") + .append(SECONDARY_SPAN_STYLE).append("'>").append(HtmlEscapeUtil.escape(scanIssue.getSeverity())) + .append(" ").append(HtmlEscapeUtil.escape(secondaryText)).append("</span></p></td></tr></table>"); + } + + /** + * Builds the vulnerability section of a scan issue description and appends it + * to the provided StringBuilder. This method processes the list of + * vulnerabilities associated with the scan issue, categorizes them by severity, + * and includes detailed descriptions for specific vulnerabilities where + * applicable. + * + * @param descBuilder the StringBuilder to which the formatted vulnerability + * section will be appended + * @param scanIssue the ScanIssue object containing details about the scan, + * including associated vulnerabilities + */ + private void buildVulnerabilitySection(StringBuilder descBuilder, ScanIssue scanIssue) { + List<Vulnerability> vulnerabilityList = scanIssue.getVulnerabilities(); + if (vulnerabilityList == null || vulnerabilityList.isEmpty()) { + return; + } + descBuilder.append("<div>").append(TABLE_WITH_TR); + Map<String, Long> vulnerabilityCount = getVulnerabilityCount(vulnerabilityList); + DESCRIPTION_ICON.forEach((severity, iconPath) -> { + Long count = vulnerabilityCount.get(severity); + if (count != null && count > 0) { + descBuilder.append("<td style='padding:4px 1px 0 0;'>") + .append(DESCRIPTION_ICON.get(getSeverityCountIconKey(severity))).append("</td>") + .append("<td style='font-size:10px;color:#A0A0A0;vertical-align:middle;padding:2px 4px 0 1px;'>") + .append(count).append("</td>"); + } + }); + descBuilder.append("</tr></table></div>"); + } + + /** + * ASCA description. Format: [Title for multiple issues] [Severity Icon] Title + * (bold) - description - SAST vulnerability + */ + private void buildASCADescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) { + String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE); + descBuilder.append(TABLE_WITH_TR_IAC_ASCA) + .append("<td style='width:20px;padding:0 6px 0 0;vertical-align:top;'>").append(severityIcon) + .append("</td>"); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder.append("<td style='padding:0 6px 0 6px;").append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY) + .append(";").append(CELL_LINE_HEIGHT_STYLE).append("'>") + .append("<div style='display:block;word-break:break-word;overflow-wrap:anywhere;white-space:normal;'>") + .append("<p style=\"").append(colorStyle).append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY) + .append(";margin:0;padding:0;line-height:1.4;\">").append("<b>") + .append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("</b>").append(" - ") + .append(HtmlEscapeUtil.escape(vulnerability.getDescription())).append(" - <span style='") + .append(SECONDARY_SPAN_STYLE).append("'>SAST vulnerability</span>").append("</p>") + .append("</div></td></tr></table>"); + buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(), scanIssue.getScanEngine().name()); + } + } + + + /** + * Secrets description. Format: [Severity Icon] Title (bold) - Secret finding + */ + private void buildSecretsDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String icon = getSeverityIconHtml(scanIssue.getSeverity(), ICON_INLINE_STYLE); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder.append(TABLE_WITH_TR).append("<td style='padding:0 6px 0 0;vertical-align:middle;'>").append(icon) + .append("</td>").append("<td style='" + colorStyle + "padding:0 2px 0 2px;").append(TITLE_FONT_SIZE) + .append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>").append("<p style='margin:0;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>").append("<b>") + .append(HtmlEscapeUtil.escape(formatTitle(scanIssue.getTitle()))).append("</b>") + .append(" - <span style='").append(SECONDARY_SPAN_STYLE).append("'>Secret finding</span>") + .append("</p></td></tr></table>"); + } + + /** + * IAC description (image header + vulnerability description with Title). + */ + private void buildIACDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) { + String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE); + descBuilder.append(TABLE_WITH_TR_IAC_ASCA) + .append("<td style='width:20px;padding:0 6px 0 0;vertical-align:middle;'>").append(severityIcon) + .append("</td>"); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder + .append("<td style='" + colorStyle + "padding:0 4px;" + "white-space:normal;" + TITLE_FONT_SIZE + + TITLE_FONT_FAMILY + ";" + CELL_LINE_HEIGHT_STYLE + "'>") + .append("<div style='" + colorStyle + "display:block;" + "word-break:break-word;" + + "overflow-wrap:anywhere;" + "'>") + .append("<b>").append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("</b>").append(" - ") + .append(HtmlEscapeUtil.escape(vulnerability.getActualValue())).append(" ") + .append(HtmlEscapeUtil.escape(vulnerability.getDescription())) + .append(" <span style='" + TITLE_FONT_SIZE + TITLE_FONT_FAMILY + ";" + CELL_LINE_HEIGHT_STYLE) + .append(SECONDARY_SPAN_STYLE).append("'> IaC vulnerability</span>") + .append("</div></td></tr></table>"); + buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(), + scanIssue.getScanEngine().name()); + } + } + + /** + * Container description (image header + vulnerability counts). + */ + private void buildContainerDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + buildImageHeader(descBuilder, scanIssue, textColor); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + /** + * Builds the default description for a scan issue and appends it to the + * provided StringBuilder. This method formats basic details about the scan + * issue, including its title and description. + * + * @param descBuilder the StringBuilder to which the formatted default + * description will be appended + * @param scanIssue the ScanIssue object containing details about the issue + * such as title and description + * Default fallback description. + */ + private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append("<div><b>") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())) + .append("</b> - ") + .append(HtmlEscapeUtil.escape(scanIssue.getDescription())) + .append("</div>"); + } + + /** + * Container image header. + */ + private void buildImageHeader(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String icon = getSeverityIconHtml(CONTAINER, ICON_INLINE_STYLE); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + + descBuilder.append(TABLE_WITH_TR).append("<td style='padding:0 6px 0 0;vertical-align:middle;'>").append(icon) + .append("</td>").append("<td style='" + colorStyle + "padding:0 2px 0 2px;").append(TITLE_FONT_SIZE) + .append(TITLE_FONT_FAMILY).append(CELL_LINE_HEIGHT_STYLE).append("'>").append("<p style='margin:0;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>").append("<b>") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append(":") + .append(HtmlEscapeUtil.escape(scanIssue.getImageTag())).append("</b>").append(" - ") + .append("<span style='").append(SECONDARY_SPAN_STYLE).append("'>") + .append(HtmlEscapeUtil.escape(scanIssue.getSeverity())) + .append(" severity image</span>").append("</p></td></tr></table>"); + } + + /** + * Calculates the count of vulnerabilities grouped by their severity levels. + * This method processes a list of vulnerabilities, retrieves their severity, + * and returns a map where the keys are severity levels and the values are the + * counts. + * + * @param vulnerabilityList the list of vulnerabilities to be grouped and + * counted by severity + * @return a map where the key is the severity level and the value is the count + * of vulnerabilities at that severity + */ + private Map<String, Long> getVulnerabilityCount(List<Vulnerability> vulnerabilityList) { + return vulnerabilityList.stream().map(Vulnerability::getSeverity) + .collect(Collectors.groupingBy(severity -> severity, Collectors.counting())); + } + + /** + * Legacy overload for backward compatibility: defaults to informational action + * links (non-clickable) and no text color override. + */ + public String formatDescriptionHtml(ScanIssue issue) { + return formatDescriptionHtml(issue, false, null); + } + + + /** + * Builds the remediation actions section of the description. + * + * @param descBuilder {@link StringBuilder} object to add the remediation + * actions section to. + * @param scanIssueId {@link String} object containing the remediation actions + * section data. + */ + private void buildRemediationActionsSection(StringBuilder descBuilder, String scanIssueId, String engineName) { + String buttonStyle = "color: #4B9CFF; cursor: pointer; " + TITLE_FONT_SIZE + TITLE_FONT_FAMILY + + CELL_LINE_HEIGHT_STYLE + "white-space: nowrap; margin:0; padding:0;"; + + // Add CSS for hover effect with underline - more specific selector with !important to ensure it applies + descBuilder.append("<style>") + .append("a[href^='#cxonedevassist'] { text-decoration: none; }") + .append("a[href^='#cxonedevassist']:hover { text-decoration: underline !important; }") + .append("</style>"); + + descBuilder.append( + "<table style='display:block;margin:8px 0 0 0;border-collapse:collapse;border-spacing:0;padding:0;'><tr>") + .append("<td style='padding:0 10px 0 0;margin:0;'>").append("<a href=\"#cxonedevassist/copyfixprompt") + .append(SEPERATOR).append(scanIssueId).append(SEPERATOR).append(engineName).append("\" style='") + .append(buttonStyle).append(TITLE_FONT_FAMILY).append("'>") + .append(DevAssistUtils.getAssistQuickFixName()).append("</a></td>") + .append("<td style='padding:0 10px 0 0;margin:0;'>").append("<a href=\"#cxonedevassist/viewdetails") + .append(SEPERATOR).append(scanIssueId).append(SEPERATOR).append(engineName).append("\" style='").append(buttonStyle) + .append(TITLE_FONT_FAMILY).append("'>").append(DevAssistConstants.VIEW_DETAILS_FIX_NAME) + .append("</a></td>").append("<td style='padding:0 10px 0 0;margin:0;'>") + .append("<a href=\"#cxonedevassist/ignorethis").append(SEPERATOR).append(scanIssueId).append(SEPERATOR) + .append("\" style='").append(buttonStyle).append(TITLE_FONT_FAMILY).append("'>") + .append(DevAssistConstants.IGNORE_THIS_VULNERABILITY_FIX_NAME).append("</a></td>"); + if (engineName.equalsIgnoreCase(String.valueOf(ScanEngine.OSS)) + || engineName.equalsIgnoreCase(String.valueOf(ScanEngine.CONTAINERS))) { + descBuilder.append("<td style='padding:0 5px 0 0;margin:0;'>") + .append("<a href=\"#cxonedevassist/ignoreallofthis").append(SEPERATOR).append(scanIssueId) + .append(SEPERATOR).append("\" style='").append(buttonStyle).append(TITLE_FONT_FAMILY).append("'>") + .append(DevAssistConstants.IGNORE_ALL_OF_THIS_TYPE_FIX_NAME) + .append("</a></td>"); + } + descBuilder.append("</tr></table><br>"); + } + + + /** + * Injects inline styles into an existing HTML image tag. + */ + private static String getSeverityIconHtml(String key, String extraStyle) { + String imgTag = DESCRIPTION_ICON.getOrDefault(key, ""); + + if (imgTag == null || imgTag.isEmpty()) { + return ""; + } + + if (imgTag.contains("style='")) { + return imgTag.replaceFirst("style='", "style='" + extraStyle); + } else if (imgTag.contains("style=\"")) { + return imgTag.replaceFirst("style=\"", "style=\"" + extraStyle); + } else { + int insertPos = imgTag.indexOf("/>"); + + return insertPos > 0 + ? imgTag.substring(0, insertPos) + " style='" + extraStyle + "'" + imgTag.substring(insertPos) + : imgTag; + } + } + + /** + * Inline styles matching JetBrains' ProblemDescription.InlineStyle. Ensures + * visual consistency with JetBrains plugin design. + */ + static class InlineStyle { + + private InlineStyle() { + } + + // Table layout: icon (20px) in first column, content in second column + static final String TABLE_WITH_TR = "<table style='display:inline-table;vertical-align:middle;border-collapse:collapse;'><tr>"; + static final String TABLE_WITH_TR_IAC_ASCA = "<table cellspacing='0' cellpadding='0' " + "style='display:table;" + + "border-collapse:collapse;" + "table-layout:auto;" + "width:100%;'><tr>"; + + static final String TABLE_WITH_TR_FULL_WIDTH = "<table cellspacing='0' cellpadding='0' " + + "style='display:table;" + "border-collapse:collapse;" + "table-layout:auto;" + "width:100%;'><tr>"; + + // Typography styles + static final String TITLE_FONT_FAMILY = "font-family: sans-serif"; + static final String TITLE_FONT_SIZE = "font-size:12px;"; + static final String CELL_LINE_HEIGHT_STYLE = "line-height:16px;vertical-align:middle;"; + + // Secondary text (severity labels like "SAST vulnerability", "IaC + // vulnerability") + static final String SECONDARY_SPAN_STYLE = "display:inline-block;vertical-align:middle;line-height:16px;font-size:11px;color:#ADADAD;" + + "font-family:system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;"; + + // Icon column style (20px wide, right-padded) + static final String ICON_COLUMN_STYLE = "width:20px;padding:0 6px 0 0;vertical-align:middle;"; + + // Content column style + static final String CONTENT_COLUMN_STYLE = "padding:0 4px;white-space:normal;" + TITLE_FONT_SIZE + + TITLE_FONT_FAMILY + CELL_LINE_HEIGHT_STYLE; + + /** + * Default inline severity icon style used consistently across all engines. + */ + static final String ICON_INLINE_STYLE = "display:inline-block;vertical-align:middle;max-height:16px;line-height:16px;"; + } + + /** + * Appends multiple violations title for ASCA and IAC engines when there are + * multiple vulnerabilities. This method adds a formatted title showing the + * number of violations detected. + * + * @param descBuilder the StringBuilder to append the title to + * @param scanIssue the ScanIssue containing information about vulnerabilities + */ + private static void appendMultipleViolationsTitle(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + if (scanIssue.getVulnerabilities() == null || scanIssue.getVulnerabilities().size() <= 1) { + return; + } + boolean isASCAOrIAC = scanIssue.getScanEngine() == ScanEngine.ASCA + || scanIssue.getScanEngine() == ScanEngine.IAC; + if (isASCAOrIAC) { + descBuilder.append(TABLE_WITH_TR).append("<td style='" + colorStyle + "padding:0 6px 0 0;'>").append("<p style='margin:0;") + .append(TITLE_FONT_SIZE).append(TITLE_FONT_FAMILY).append("'>") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append(" <span style='") + .append(SECONDARY_SPAN_STYLE).append("'>Checkmarx One Assist</span>") + .append("</p></td></tr></table>"); + } + } + + /** + * Generates an HTML image element based on the provided icon name. + * + * @param iconPath the path to the image file that will be used in the HTML + * content + * @return a String representing an HTML image element with the provided icon + * path + */ + private static String getImage(String iconPath) { + String imagePath = DevAssistUtils.themeBasedPNGIconForHtmlImage(iconPath); + if (imagePath == null || imagePath.isEmpty()) { + return ""; + } + try { + URL imageUrl = new URL(imagePath); + if (imageUrl != null) { + URL fileUrl = FileLocator.toFileURL(imageUrl); + String urlString = fileUrl.toString(); + return "<img src='" + urlString + "' " + "style='display:inline-block;vertical-align:middle;' />"; + } + } catch (Exception e) { + return ""; + } + return ""; + } + + /** + * Formats a kebab-case title into Title-Case (e.g., "generic-api-key" -> + * "Generic-Api-Key"). + * + * @param title The kebab-case title string. + * @return A formatted Title-Case string. + */ + private String formatTitle(String title) { + if (title == null || title.isEmpty()) { + return ""; + } + return Arrays.stream(title.split("-")).map( + word -> word.isEmpty() ? "" : Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase()) + .collect(Collectors.joining("-")); + } + + /** + * Returns the key for the icon representing the specified severity with a count + * suffix. + * + * @param severity the severity + * @return the key for the icon representing the specified severity with a count + * suffix + */ + private static String getSeverityCountIconKey(String severity) { + return severity + COUNT; + } + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java new file mode 100644 index 00000000..c477bd9d --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java @@ -0,0 +1,241 @@ +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; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + +/** + * 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) - light and dark variants + registerIcon("malicious_16", "icons/severity_16/malicious.svg"); + registerIcon("malicious_16_dark", "icons/severity_16/malicious_dark.svg"); + registerIcon("critical_16", "icons/severity_16/critical.svg"); + registerIcon("critical_16_dark", "icons/severity_16/critical_dark.svg"); + registerIcon("high_16", "icons/severity_16/high.svg"); + registerIcon("high_16_dark", "icons/severity_16/high_dark.svg"); + registerIcon("medium_16", "icons/severity_16/medium.svg"); + registerIcon("medium_16_dark", "icons/severity_16/medium_dark.svg"); + registerIcon("low_16", "icons/severity_16/low.svg"); + registerIcon("low_16_dark", "icons/severity_16/low_dark.svg"); + + // Register medium icons (20px) - light and dark variants + registerIcon("malicious_20", "icons/severity_20/malicious.svg"); + registerIcon("malicious_20_dark", "icons/severity_20/malicious_dark.svg"); + registerIcon("critical_20", "icons/severity_20/critical.svg"); + registerIcon("critical_20_dark", "icons/severity_20/critical_dark.svg"); + registerIcon("high_20", "icons/severity_20/high.svg"); + registerIcon("high_20_dark", "icons/severity_20/high_dark.svg"); + registerIcon("medium_20", "icons/severity_20/medium.svg"); + registerIcon("medium_20_dark", "icons/severity_20/medium_dark.svg"); + registerIcon("low_20", "icons/severity_20/low.svg"); + registerIcon("low_20_dark", "icons/severity_20/low_dark.svg"); + + // Register base icons - light and dark variants + registerIcon("malicious", "icons/severity/malicious.svg"); + registerIcon("malicious_dark", "icons/severity/malicious_dark.svg"); + registerIcon("critical", "icons/severity/critical.svg"); + registerIcon("critical_dark", "icons/severity/critical_dark.svg"); + registerIcon("high", "icons/severity/high.svg"); + registerIcon("high_dark", "icons/severity/high_dark.svg"); + registerIcon("medium", "icons/severity/medium.svg"); + registerIcon("medium_dark", "icons/severity/medium_dark.svg"); + registerIcon("low", "icons/severity/low.svg"); + registerIcon("low_dark", "icons/severity/low_dark.svg"); + + registerIcon("star_action", "icons/start-action.svg"); + registerIcon("devassistBadge", "icons/devassist_badge.svg"); + + // Register card icons for ignored findings (package, secret, containers, vulnerability) + // Package icons (OSS) + registerIcon("card_package_critical", "icons/ignored_card/card-package-critical.svg"); + registerIcon("card_package_critical_dark", "icons/ignored_card/card-package-critical_dark.svg"); + registerIcon("card_package_high", "icons/ignored_card/card-package-high.svg"); + registerIcon("card_package_high_dark", "icons/ignored_card/card-package-high_dark.svg"); + registerIcon("card_package_medium", "icons/ignored_card/card-package-medium.svg"); + registerIcon("card_package_medium_dark", "icons/ignored_card/card-package-medium_dark.svg"); + registerIcon("card_package_low", "icons/ignored_card/card-package-low.svg"); + registerIcon("card_package_low_dark", "icons/ignored_card/card-package-low_dark.svg"); + registerIcon("card_package_malicious", "icons/ignored_card/card-package-malicious.svg"); + registerIcon("card_package_malicious_dark", "icons/ignored_card/card-package-malicious_dark.svg"); + + // Secret icons (SECRETS) + registerIcon("card_secret_critical", "icons/ignored_card/card-secret-critical.svg"); + registerIcon("card_secret_critical_dark", "icons/ignored_card/card-secret-critical_dark.svg"); + registerIcon("card_secret_high", "icons/ignored_card/card-secret-high.svg"); + registerIcon("card_secret_high_dark", "icons/ignored_card/card-secret-high_dark.svg"); + registerIcon("card_secret_medium", "icons/ignored_card/card-secret-medium.svg"); + registerIcon("card_secret_medium_dark", "icons/ignored_card/card-secret-medium_dark.svg"); + registerIcon("card_secret_low", "icons/ignored_card/card-secret-low.svg"); + registerIcon("card_secret_low_dark", "icons/ignored_card/card-secret-low_dark.svg"); + registerIcon("card_secret_malicious", "icons/ignored_card/card-secret-malicious.svg"); + registerIcon("card_secret_malicious_dark", "icons/ignored_card/card-secret-malicious_dark.svg"); + + // Container icons (CONTAINERS) + registerIcon("card_containers_critical", "icons/ignored_card/card-containers-critical.svg"); + registerIcon("card_containers_critical_dark", "icons/ignored_card/card-containers-critical_dark.svg"); + registerIcon("card_containers_high", "icons/ignored_card/card-containers-high.svg"); + registerIcon("card_containers_high_dark", "icons/ignored_card/card-containers-high_dark.svg"); + registerIcon("card_containers_medium", "icons/ignored_card/card-containers-medium.svg"); + registerIcon("card_containers_medium_dark", "icons/ignored_card/card-containers-medium_dark.svg"); + registerIcon("card_containers_low", "icons/ignored_card/card-containers-low.svg"); + registerIcon("card_containers_low_dark", "icons/ignored_card/card-containers-low_dark.svg"); + registerIcon("card_containers_malicious", "icons/ignored_card/card-containers-malicious.svg"); + registerIcon("card_containers_malicious_dark", "icons/ignored_card/card-containers-malicious_dark.svg"); + + // Vulnerability icons (IAC/ASCA) + registerIcon("card_vulnerability_critical", "icons/ignored_card/card-vulnerability-critical.svg"); + registerIcon("card_vulnerability_critical_dark", "icons/ignored_card/card-vulnerability-critical_dark.svg"); + registerIcon("card_vulnerability_high", "icons/ignored_card/card-vulnerability-high.svg"); + registerIcon("card_vulnerability_high_dark", "icons/ignored_card/card-vulnerability-high_dark.svg"); + registerIcon("card_vulnerability_medium", "icons/ignored_card/card-vulnerability-medium.svg"); + registerIcon("card_vulnerability_medium_dark", "icons/ignored_card/card-vulnerability-medium_dark.svg"); + registerIcon("card_vulnerability_low", "icons/ignored_card/card-vulnerability-low.svg"); + registerIcon("card_vulnerability_low_dark", "icons/ignored_card/card-vulnerability-low_dark.svg"); + registerIcon("card_vulnerability_malicious", "icons/ignored_card/card-vulnerability-malicious.svg"); + registerIcon("card_vulnerability_malicious_dark", "icons/ignored_card/card-vulnerability-malicious_dark.svg"); + } + + private static void registerIcon(String key, String path) { + // Load icons from devassist module instead of main plugin + imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin("com.checkmarx.eclipse.devassist", 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 theme-aware icon for a severity level and size. + * Returns dark variant in dark theme, light variant in light theme. + * + * @param severity Severity level (case-insensitive) + * @param size Icon size + * @return Image instance or null if not found + */ + public static Image getThemeAwareIcon(String severity, Size size) { + if (severity == null) { + return null; + } + + String key = severity.toLowerCase() + size.getSuffix(); + + // Append _dark suffix if dark theme is active + if (DevAssistUtils.isDarkTheme()) { + key += "_dark"; + } + + return imageRegistry.get(key); + } + + /** + * Get icon for a severity level with default small size. + * + * @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; + } + + /** + * Get card icon for a given type and severity (theme-aware). + * + * @param type Type of scan (OSS, SECRETS, CONTAINERS, ASCA, IAC) + * @param severity Severity level + * @return Image instance or null if not found + */ + public static Image getCardIcon(String type, String severity) { + if (type == null || severity == null) { + return null; + } + + String prefix = switch (type.toUpperCase()) { + case "OSS" -> "card_package"; + case "SECRETS" -> "card_secret"; + case "CONTAINERS" -> "card_containers"; + case "ASCA", "IAC" -> "card_vulnerability"; + default -> "card_vulnerability"; + }; + + String key = prefix + "_" + severity.toLowerCase(); + + // Append _dark suffix if dark theme is active + if (DevAssistUtils.isDarkTheme()) { + key += "_dark"; + } + + return imageRegistry.get(key); + } +} 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..58621112 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java @@ -0,0 +1,275 @@ +package com.checkmarx.eclipse.devassist.ui.findings.icons; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Display; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; + +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<String, Image> compositeImageCache = new HashMap<>(); + + // Shared severity icon instances + private static final Image MALICIOUS_ICON = IconRegistry.getIcon(DevAssistConstants.MALICIOUS, + IconRegistry.Size.SMALL); + private static final Image CRITICAL_ICON = IconRegistry.getIcon(DevAssistConstants.CRITICAL, + IconRegistry.Size.SMALL); + private static final Image HIGH_ICON = IconRegistry.getIcon(DevAssistConstants.HIGH, IconRegistry.Size.SMALL); + private static final Image MEDIUM_ICON = IconRegistry.getIcon(DevAssistConstants.MEDIUM, IconRegistry.Size.SMALL); + private static final Image LOW_ICON = IconRegistry.getIcon(DevAssistConstants.LOW, IconRegistry.Size.SMALL); + + /** + * 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; + } + + // Create cache key with a prefix to avoid collisions with + // createSeverityBadgeImage + String cacheKey = "full_" + createCacheKey(fileNode); + if (compositeImageCache.containsKey(cacheKey)) { + return compositeImageCache.get(cacheKey); + } + + try { + Display display = Display.getDefault(); + Image compositeImage = createFullBadgeImage(display, fileNode); + + if (compositeImage != null) { + compositeImageCache.put(cacheKey, compositeImage); + } + + 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 { + + // Calculate total width needed + int iconSize = 16; + int spacing = 1; + int width = 0; + + if (hasCount(fileNode, DevAssistConstants.MALICIOUS)) { + width += iconSize + spacing; + } + if (hasCount(fileNode, DevAssistConstants.CRITICAL)) { + width += iconSize + spacing; + } + if (hasCount(fileNode, DevAssistConstants.HIGH)) { + width += iconSize + spacing; + } + if (hasCount(fileNode, DevAssistConstants.MEDIUM)) { + width += iconSize + spacing; + } + if (hasCount(fileNode, DevAssistConstants.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; + + if (hasCount(fileNode, "malicious") && MALICIOUS_ICON != null) { + gc.drawImage(MALICIOUS_ICON, x, y); + x += iconSize + spacing; + } + + // Draw critical icon if count > 0 + if (hasCount(fileNode, "critical") && CRITICAL_ICON != null) { + gc.drawImage(CRITICAL_ICON, x, y); + x += iconSize + spacing; + } + + // Draw high icon if count > 0 + if (hasCount(fileNode, "high") && HIGH_ICON != null) { + gc.drawImage(HIGH_ICON, x, y); + x += iconSize + spacing; + } + + // Draw medium icon if count > 0 + if (hasCount(fileNode, "medium") && MEDIUM_ICON != null) { + gc.drawImage(MEDIUM_ICON, x, y); + x += iconSize + spacing; + } + + // Draw low icon if count > 0 + if (hasCount(fileNode, "low") && LOW_ICON != null) { + gc.drawImage(LOW_ICON, 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 { + + // 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, DevAssistConstants.MALICIOUS)) + iconCount++; + if (hasCount(fileNode, DevAssistConstants.CRITICAL)) + iconCount++; + if (hasCount(fileNode, DevAssistConstants.HIGH)) + iconCount++; + if (hasCount(fileNode, DevAssistConstants.MEDIUM)) + iconCount++; + if (hasCount(fileNode, DevAssistConstants.LOW)) + iconCount++; + + if (iconCount == 0) { + return null; + } + + // 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; + + if (hasCount(fileNode, DevAssistConstants.MALICIOUS) && MALICIOUS_ICON != null) { + gc.drawImage(MALICIOUS_ICON, x, y); + x += iconSize + spacing; + } + + // Draw critical icon + if (hasCount(fileNode, DevAssistConstants.CRITICAL) && CRITICAL_ICON != null) { + gc.drawImage(CRITICAL_ICON, x, y); + x += iconSize + spacing; + } + + // Draw high icon + if (hasCount(fileNode, DevAssistConstants.HIGH) && HIGH_ICON != null) { + gc.drawImage(HIGH_ICON, x, y); + x += iconSize + spacing; + } + + // Draw medium icon + if (hasCount(fileNode, DevAssistConstants.MEDIUM) && MEDIUM_ICON != null) { + gc.drawImage(MEDIUM_ICON, x, y); + x += iconSize + spacing; + } + + // Draw low icon + if (hasCount(fileNode, DevAssistConstants.LOW) && LOW_ICON != null) { + gc.drawImage(LOW_ICON, 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("m:").append(fileNode.getProblemCount().getOrDefault("malicious", 0L)).append("|"); + 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/ignore/DevAssistIgnoredFindings.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/ignore/DevAssistIgnoredFindings.java new file mode 100644 index 00000000..0371ef05 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/ignore/DevAssistIgnoredFindings.java @@ -0,0 +1,1078 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignore; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +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.CoreException; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +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.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.graphics.Font; +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.Link; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorPart; +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.ui.part.ViewPart; +import org.eclipse.ui.plugin.AbstractUIPlugin; + +import com.checkmarx.eclipse.common.events.SettingsTopics; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.Constants; +import com.checkmarx.eclipse.devassist.ignore.IgnoreEntry; +import com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +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.icons.IconRegistry; +import com.checkmarx.eclipse.devassist.utils.DateFormatUtil; + +/** + * Tool window panel for viewing ignored vulnerability findings. Features strict + * column alignment, rounded badge styling, dynamic dates, and centered empty + * state. + */ +public class DevAssistIgnoredFindings extends ViewPart { + + public static final String ID = "com.checkmarx.eclipse.devassist.ui.findings.ignore.DevAssistIgnoredFindings"; + + /** + * Refreshes this view's tab title/entry list right away if it's currently + * open, instead of relying solely on {@link IgnoreFileManager}'s listener + * notification. A brand-new project's {@code IgnoreFileManager} instance is + * created lazily by whichever caller (this view or the ignore action + * itself) resolves it first - if the ignore action gets there first, this + * view's listener isn't attached to it yet at that moment, so its very + * first "onIgnoreUpdated" notification would otherwise be missed. Ignore + * call sites (context-menu "Ignore"/"Ignore All", hover quick-fix links) + * call this directly, mirroring the existing CxFindingsView refresh-on-ignore + * pattern, so the tab count updates on the very first ignore too - not just + * from the second one onward once the listener happens to already be wired + * up. + */ + public static void refreshIfOpen() { + 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.IViewPart view = page.findView(ID); + if (view instanceof DevAssistIgnoredFindings) { + ((DevAssistIgnoredFindings) view).refreshTable(); + } + } catch (Exception e) { + CxLogger.warning("[IGNORED-FINDINGS] Error refreshing view after ignore action: " + e.getMessage()); + } + } + + private Composite parentComposite; + private Composite container; + private Composite openSettingsComposite; + private Shell shell; + private static final Image CHECKMARX_OPEN_SETTINGS_LOGO = AbstractUIPlugin + .imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, "/icons/checkmarx-80.png").createImage(); + private org.osgi.service.event.EventHandler settingsEventHandler; + + // Top selection action bar + private Composite selectionActionBar; + private Label selectionCountLabel; + private Button clearSelectionButton; + private Button reviveSelectedButton; + + // Header row components + private Composite headerComposite; + private Button selectAllButton; + private Label riskHeaderLabel; + private Label lastUpdatedHeaderLabel; + + private ScrolledComposite scrolledContainer; + private Composite cardsContainer; + private Label emptyLabel; + + private IProject currentProject; + private IgnoreFileManager ignoreFileManager; + private final IgnoreFileManager.IgnoreListener ignoreListener = this::onIgnoreDataUpdated; + // Reacts to project open/close so the view stops reading the now-closed + // project's (stale/cached) IgnoreFileManager and switches to whatever + // project is open now, instead of only re-resolving on the next incidental + // refreshTable() call (e.g. setFocus()). + private final IResourceChangeListener projectStateListener = this::handleWorkspaceResourceChange; + + // Independent from VulnerabilityFilterState.getInstance() (used by the main + // Findings view) so toggling a severity here doesn't also filter that view. + private final VulnerabilityFilterState filterState = new VulnerabilityFilterState(); + + private List<IgnoreEntryCard> cards = new ArrayList<>(); + // Keyed by the ignore-file map key (stable identity for an entry) rather than + // the IgnoreEntry instance itself - refreshFromDisk() re-deserializes the + // ignore file into brand-new IgnoreEntry objects on every call (even a no-op + // refresh triggered by setFocus() when the user switches back to this view), + // so identity/equality-based tracking would silently drop the selection. + private Set<String> selectedKeys = new HashSet<>(); + private boolean isProgrammaticSelectionChange = false; + + @Override + public void createPartControl(Composite parent) { + this.parentComposite = parent; + this.shell = parent.getShell(); + + GridLayout parentLayout = new GridLayout(1, true); + parentLayout.marginWidth = 0; + parentLayout.marginHeight = 0; + parent.setLayout(parentLayout); + + subscribeToSettingsEvents(); + ResourcesPlugin.getWorkspace().addResourceChangeListener(projectStateListener, + IResourceChangeEvent.POST_CHANGE); + + ensureProjectAndIgnoreManager(); + + refreshViewMode(); + } + + /** + * Detects project open/close transitions and refreshes the view so it stops + * showing data read from a project that just closed. Runs on the workspace + * notification thread, so the actual refresh is marshalled back to the UI + * thread. + */ + private void handleWorkspaceResourceChange(IResourceChangeEvent event) { + IResourceDelta delta = event.getDelta(); + if (delta == null) { + return; + } + boolean[] projectOpenStateChanged = { false }; + try { + delta.accept(d -> { + if (d.getResource() instanceof IProject && (d.getFlags() & IResourceDelta.OPEN) != 0) { + projectOpenStateChanged[0] = true; + } + return true; + }); + } catch (CoreException e) { + CxLogger.warning("[IGNORED-FINDINGS] Error inspecting resource delta: " + e.getMessage()); + } + if (!projectOpenStateChanged[0]) { + return; + } + // Called on the workspace notification thread - don't touch any SWT widget + // (including container.getDisplay()) until inside asyncExec. + Display display = Display.getDefault(); + if (display == null || display.isDisposed()) { + return; + } + display.asyncExec(() -> { + if (container != null && !container.isDisposed()) { + refreshTable(); + } + }); + } + + /** + * Determines which panel to draw based on current credentials status, + * mirroring {@code CxFindingsView#refreshViewMode} - so the Ignored Findings + * window also gates on authentication instead of always showing the ignored + * entries list (or an empty state) while logged out. + */ + private void refreshViewMode() { + if (parentComposite == null || parentComposite.isDisposed()) { + return; + } + if (!Preferences.isAuthenticated()) { + drawMissingCredentialsPanel(parentComposite); + } else { + drawIgnoredFindingsPanel(parentComposite); + } + } + + /** + * Subscribes to the settings-applied event topic so logging in/out toggles + * between the missing-credentials panel and the ignored-findings list live, + * without requiring the view to be closed and reopened. + */ + private void subscribeToSettingsEvents() { + 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) { + settingsEventHandler = event -> Display.getDefault().asyncExec(this::refreshViewMode); + eventBroker.subscribe(SettingsTopics.TOPIC_APPLY_SETTINGS, settingsEventHandler); + } + } catch (Exception e) { + System.err.println("[IGNORED-FINDINGS] Error subscribing to IEventBroker: " + e.getMessage()); + } + } + + /** + * Renders the missing credentials panel, matching {@code CxFindingsView}'s + * look (logo + Open Settings button) so both views behave consistently while + * logged out. + */ + private void drawMissingCredentialsPanel(Composite parent) { + for (Control child : parent.getChildren()) { + child.dispose(); + } + clearToolbarContributions(); + + openSettingsComposite = new Composite(parent, SWT.NONE); + openSettingsComposite.setLayout(new GridLayout(1, true)); + openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + + Label cxLogo = new Label(openSettingsComposite, SWT.NONE); + cxLogo.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + cxLogo.setImage(CHECKMARX_OPEN_SETTINGS_LOGO); + + 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(); + } + }); + + setPartName("Ignored Findings"); + parent.layout(true, true); + } + + /** + * Removes every contribution from the view toolbar - used while the missing + * credentials panel is showing, since the severity filter actions operate on + * an ignored-entries list that isn't rendered in that state. + */ + private void clearToolbarContributions() { + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + toolbar.removeAll(); + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + } + + private void drawIgnoredFindingsPanel(Composite parent) { + // Clear out the missing-credentials panel if it was showing + for (Control child : parent.getChildren()) { + child.dispose(); + } + + container = new Composite(parent, SWT.NONE); + container.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + GridLayout containerLayout = new GridLayout(1, false); + containerLayout.marginWidth = 12; + containerLayout.marginHeight = 10; + container.setLayout(containerLayout); + + // ----------------------------------------------------------------- + // 1. TOP SELECTION ACTION BAR (Visible only when items are selected) + // ----------------------------------------------------------------- + selectionActionBar = new Composite(container, SWT.NONE); + GridLayout actionBarLayout = new GridLayout(3, false); + actionBarLayout.marginWidth = 0; + actionBarLayout.marginHeight = 0; + selectionActionBar.setLayout(actionBarLayout); + + GridData actionBarData = new GridData(SWT.FILL, SWT.TOP, true, false); + actionBarData.exclude = true; + selectionActionBar.setLayoutData(actionBarData); + selectionActionBar.setVisible(false); + + selectionCountLabel = new Label(selectionActionBar, SWT.NONE); + selectionCountLabel.setText("0 Risk selected |"); + selectionCountLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + clearSelectionButton = new Button(selectionActionBar, SWT.PUSH | SWT.FLAT); + clearSelectionButton.setText("✕ Clear Selections"); + clearSelectionButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); + clearSelectionButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + clearAllSelections(); + } + }); + + reviveSelectedButton = new Button(selectionActionBar, SWT.PUSH); + reviveSelectedButton.setText("« Revive Selected"); + reviveSelectedButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); + reviveSelectedButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + reviveSelected(); + } + }); + + // ----------------------------------------------------------------- + // 2. COLUMN HEADERS ROW (Strict Grid Alignment) + // ----------------------------------------------------------------- + headerComposite = new Composite(container, SWT.NONE); + GridLayout headerLayout = new GridLayout(4, false); + headerLayout.marginWidth = 0; + headerLayout.marginHeight = 4; + headerLayout.horizontalSpacing = 16; + headerComposite.setLayout(headerLayout); + headerComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + + // Col 1 Header: Checkbox + selectAllButton = new Button(headerComposite, SWT.CHECK); + GridData col1HeaderData = new GridData(SWT.LEFT, SWT.CENTER, false, false); + selectAllButton.setLayoutData(col1HeaderData); + selectAllButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onSelectAllToggled(selectAllButton.getSelection()); + } + }); + + // Col 2 Header: Risk + riskHeaderLabel = new Label(headerComposite, SWT.NONE); + riskHeaderLabel.setText("Risk"); + riskHeaderLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + GridData col2HeaderData = new GridData(SWT.FILL, SWT.CENTER, true, false); + riskHeaderLabel.setLayoutData(col2HeaderData); + + // Col 3 Header: Last Updated + lastUpdatedHeaderLabel = new Label(headerComposite, SWT.NONE); + lastUpdatedHeaderLabel.setText("Last Updated"); + lastUpdatedHeaderLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + GridData col3HeaderData = new GridData(SWT.RIGHT, SWT.CENTER, false, false); + col3HeaderData.widthHint = 110; + lastUpdatedHeaderLabel.setLayoutData(col3HeaderData); + + // Col 4 Header: Action Spacer + Label reviveHeaderPlaceholder = new Label(headerComposite, SWT.NONE); + GridData col4HeaderData = new GridData(SWT.RIGHT, SWT.CENTER, false, false); + col4HeaderData.widthHint = 95; + reviveHeaderPlaceholder.setLayoutData(col4HeaderData); + + // ----------------------------------------------------------------- + // 3. EMPTY STATE & SCROLLED CARDS CONTAINER + // ----------------------------------------------------------------- + emptyLabel = new Label(container, SWT.CENTER | SWT.WRAP); + emptyLabel.setText("No ignored Findings"); + emptyLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + GridData emptyData = new GridData(SWT.CENTER, SWT.CENTER, true, true); + emptyLabel.setLayoutData(emptyData); + + scrolledContainer = new ScrolledComposite(container, SWT.V_SCROLL | SWT.H_SCROLL); + scrolledContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + scrolledContainer.setExpandHorizontal(true); + scrolledContainer.setExpandVertical(true); + + cardsContainer = new Composite(scrolledContainer, SWT.NONE); + GridLayout cardLayout = new GridLayout(1, true); + cardLayout.marginWidth = 0; + cardLayout.marginHeight = 0; + cardLayout.verticalSpacing = 16; + cardsContainer.setLayout(cardLayout); + + scrolledContainer.setContent(cardsContainer); + + // Recompute the scroll area's min height whenever the view is resized - + // without this, a width change after the cards were last measured leaves + // the wrap-label/badge-row heights (measured at the OLD width) stale, so + // the scrollbar can under- or over-shoot the real content height again. + scrolledContainer.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + updateScrolledMinHeight(); + } + }); + + setupToolbar(); + refreshTable(); + + parent.layout(true, true); + } + + /** + * Adds severity toggle-filter buttons to the view's toolbar, mirroring + * CxFindingsView's filter UX so ignored entries can be narrowed down by + * severity the same way active findings can. + */ + private void setupToolbar() { + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + toolbar.removeAll(); + + VulnerabilityFilterAction.IFilterChangeListener filterListener = this::refreshTable; + + toolbar.add(new VulnerabilityFilterAction.MaliciousFilter(filterListener, filterState)); + toolbar.add(new VulnerabilityFilterAction.CriticalFilter(filterListener, filterState)); + toolbar.add(new VulnerabilityFilterAction.HighFilter(filterListener, filterState)); + toolbar.add(new VulnerabilityFilterAction.MediumFilter(filterListener, filterState)); + toolbar.add(new VulnerabilityFilterAction.LowFilter(filterListener, filterState)); + + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + } + + private static int activeFileCount(IgnoreEntry entry) { + if (entry.getFiles() == null) { + return 0; + } + return (int) entry.getFiles().stream().filter(IgnoreEntry.FileReference::isActive).count(); + } + + private void ensureProjectAndIgnoreManager() { + if (currentProject != null && !currentProject.isOpen()) { + // The project we were reading from just closed - drop the stale + // reference (and its listener) instead of continuing to read whatever + // IgnoreFileManager instance was cached for it, so the view re-resolves + // against whatever project is actually open now. + detachIgnoreFileManager(); + currentProject = null; + } + + if (currentProject == null) { + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (project.isOpen()) { + currentProject = project; + break; + } + } + } + + if (currentProject != null && ignoreFileManager == null) { + ignoreFileManager = IgnoreFileManager.getInstance(currentProject); + ignoreFileManager.addListener(ignoreListener); + } + } + + private void detachIgnoreFileManager() { + if (ignoreFileManager != null) { + ignoreFileManager.removeListener(ignoreListener); + ignoreFileManager = null; + } + } + + public void refreshTable() { + ensureProjectAndIgnoreManager(); + + if (container == null || container.isDisposed()) { + return; + } + + List<Map.Entry<String, IgnoreEntry>> entries; + if (ignoreFileManager == null) { + // No open project to read a .checkmarxIgnored file from (e.g. the + // project that was showing here just closed) - render the empty state + // instead of leaving whatever was last drawn for the closed project on + // screen. + entries = java.util.Collections.emptyList(); + } else { + ignoreFileManager.refreshFromDisk(); + entries = ignoreFileManager.getIgnoreData().entrySet().stream() + .filter(e -> activeFileCount(e.getValue()) > 0) + .filter(e -> e.getValue().getSeverity() == null || filterState.hasFilter(e.getValue().getSeverity())) + .collect(Collectors.toList()); + } + + // Drop selections for keys that no longer correspond to a currently + // displayed entry (e.g. revived/removed elsewhere) - everything else + // carries over across this refresh, including refreshes triggered by + // setFocus() when the user returns to this view from elsewhere. + Set<String> currentKeys = entries.stream().map(Map.Entry::getKey).collect(Collectors.toSet()); + selectedKeys.retainAll(currentKeys); + + reconstructCards(entries); + + boolean hasEntries = !entries.isEmpty(); + int entryCount = entries.size(); + + // Update tab title with count + if (hasEntries) { + setPartName("Ignored Findings " + entryCount); + } else { + setPartName("Ignored Findings"); + } + + emptyLabel.setVisible(!hasEntries); + ((GridData) emptyLabel.getLayoutData()).exclude = hasEntries; + + scrolledContainer.setVisible(hasEntries); + ((GridData) scrolledContainer.getLayoutData()).exclude = !hasEntries; + + headerComposite.setVisible(hasEntries); + ((GridData) headerComposite.getLayoutData()).exclude = !hasEntries; + + if (!hasEntries) { + selectAllButton.setSelection(false); + selectAllButton.setEnabled(false); + } else { + selectAllButton.setEnabled(true); + // Reflect a carried-over "all selected" state in the header checkbox too, + // e.g. after a refresh triggered by returning to this view. + selectAllButton.setSelection(selectedKeys.size() == entryCount); + } + + container.layout(true, true); + } + + private void reconstructCards(List<Map.Entry<String, IgnoreEntry>> entries) { + for (IgnoreEntryCard card : cards) { + card.dispose(); + } + cards.clear(); + + for (Control child : cardsContainer.getChildren()) { + child.dispose(); + } + + for (Map.Entry<String, IgnoreEntry> entry : entries) { + IgnoreEntryCard card = new IgnoreEntryCard(cardsContainer, entry.getKey(), entry.getValue(), this, + selectedKeys.contains(entry.getKey())); + cards.add(card); + } + + cardsContainer.layout(true, true); + updateScrolledMinHeight(); + updateSelectionStateUI(); + } + + /** + * Recomputes the scroll area's min height after a card's content changes + * size (e.g. expanding/collapsing the "N more files" link), so the + * ScrolledComposite's scrollbar stays in sync with the actual content height. + */ + public void relayoutCards() { + if (cardsContainer == null || cardsContainer.isDisposed()) { + return; + } + cardsContainer.layout(true, true); + updateScrolledMinHeight(); + } + + /** + * Measures cardsContainer's required height at its ACTUAL rendered width, + * not an unconstrained default width. computeSize(SWT.DEFAULT, SWT.DEFAULT) + * measures wrap-labels (the description text) and the badge-row GridLayout + * at their preferred, unconstrained width, which is wider than the width + * they're actually confined to inside the scrollable area - at that wider + * "preferred" width the same text wraps LESS, under-reporting the true + * height needed. setMinHeight() then reserved less scroll space than the + * content actually needs, clipping whatever renders last (the final card's + * badge row) once scrolled to the bottom. + */ + private void updateScrolledMinHeight() { + if (scrolledContainer == null || scrolledContainer.isDisposed() + || cardsContainer == null || cardsContainer.isDisposed()) { + return; + } + int width = scrolledContainer.getClientArea().width; + Point size = width > 0 + ? cardsContainer.computeSize(width, SWT.DEFAULT) + : cardsContainer.computeSize(SWT.DEFAULT, SWT.DEFAULT); + scrolledContainer.setMinHeight(size.y); + } + + private void onSelectAllToggled(boolean selectAll) { + isProgrammaticSelectionChange = true; + try { + selectedKeys.clear(); + for (IgnoreEntryCard card : cards) { + card.setSelected(selectAll); + if (selectAll) { + selectedKeys.add(card.getKey()); + } + } + updateSelectionStateUI(); + } finally { + isProgrammaticSelectionChange = false; + } + } + + public void onCardSelectionChanged(String key, boolean selected) { + if (isProgrammaticSelectionChange) { + return; + } + + if (selected) { + selectedKeys.add(key); + } else { + selectedKeys.remove(key); + } + + if (selectedKeys.size() == cards.size() && !cards.isEmpty()) { + selectAllButton.setSelection(true); + } else { + selectAllButton.setSelection(false); + } + + updateSelectionStateUI(); + } + + private void clearAllSelections() { + onSelectAllToggled(false); + selectAllButton.setSelection(false); + } + + private void updateSelectionStateUI() { + boolean hasSelection = !selectedKeys.isEmpty(); + + selectionActionBar.setVisible(hasSelection); + ((GridData) selectionActionBar.getLayoutData()).exclude = !hasSelection; + + if (hasSelection) { + int count = selectedKeys.size(); + selectionCountLabel.setText(count + (count == 1 ? " Risk selected |" : " Risks selected |")); + } + + container.layout(true, true); + } + + public void onCardRevive(IgnoreEntry entry) { + ensureProjectAndIgnoreManager(); + if (currentProject == null) { + return; + } + IgnoreManager.getInstance(currentProject).reviveSingleEntry(entry); + refreshTable(); + } + + /** + * Navigates to the file (and line, if known) referenced by a file badge on an + * ignore entry card, mirroring the file-badge navigation already implemented + * in the JetBrains plugin's DevAssistIgnoredFindings. + */ + public void navigateToFile(IgnoreEntry.FileReference file) { + ensureProjectAndIgnoreManager(); + if (file == null || file.getPath() == null || currentProject == null) { + return; + } + + try { + java.nio.file.Path absolutePath = Paths.get(currentProject.getLocation().toOSString(), file.getPath()); + IFile ifile = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(absolutePath.toString())); + + if (ifile == null || !ifile.exists()) { + showErrorMessage("Could not find file: " + file.getPath()); + return; + } + + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IEditorPart editor = IDE.openEditor(page, ifile); + + Integer line = file.getLine(); + if (line != null && line > 0) { + scrollToLine(editor, line); + } + } catch (Exception e) { + showErrorMessage("Failed to open file: " + e.getMessage()); + } + } + + private boolean scrollToLine(IEditorPart editor, int lineNumber) { + if (editor == null || lineNumber <= 0) { + return false; + } + try { + 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()) { + int lineOffset = document.getLineOffset(lineNumber - 1); + textEditor.selectAndReveal(lineOffset, 0); + return true; + } + } + } + } catch (Exception e) { + CxLogger.error("Failed to scroll to line " + lineNumber, e); + } + return false; + } + + private void showErrorMessage(String message) { + if (shell == null || shell.isDisposed()) { + return; + } + MessageBox msgBox = new MessageBox(shell, SWT.ICON_ERROR); + msgBox.setMessage(message); + msgBox.setText("Checkmarx AI Assist"); + msgBox.open(); + } + + private void reviveSelected() { + ensureProjectAndIgnoreManager(); + if (currentProject == null || selectedKeys.isEmpty()) { + return; + } + + // Resolve the currently-selected keys against the live ignore data at the + // moment of the action, rather than reviving whatever IgnoreEntry + // instances were cached at selection time - those can go stale (e.g. the + // ignore file changing on disk between selecting and clicking Revive). + Map<String, IgnoreEntry> currentData = ignoreFileManager.getIgnoreData(); + List<IgnoreEntry> entriesToRevive = selectedKeys.stream().map(currentData::get).filter(Objects::nonNull) + .collect(Collectors.toList()); + selectedKeys.clear(); + if (entriesToRevive.isEmpty()) { + return; + } + IgnoreManager.getInstance(currentProject).reviveMultipleEntries(entriesToRevive); + refreshTable(); + } + + private void onIgnoreDataUpdated() { + Display display = container != null && !container.isDisposed() ? container.getDisplay() : Display.getDefault(); + display.asyncExec(() -> { + if (container != null && !container.isDisposed()) { + refreshTable(); + } + }); + } + + @Override + public void setFocus() { + refreshTable(); + if (cardsContainer != null && !cardsContainer.isDisposed()) { + cardsContainer.setFocus(); + } + } + + @Override + public void dispose() { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(projectStateListener); + if (settingsEventHandler != null) { + try { + org.eclipse.e4.core.services.events.IEventBroker eventBroker = PlatformUI.getWorkbench() + .getService(org.eclipse.e4.core.services.events.IEventBroker.class); + if (eventBroker != null) { + eventBroker.unsubscribe(settingsEventHandler); + } + } catch (Exception e) { + System.err.println("[IGNORED-FINDINGS] Error unsubscribing from IEventBroker: " + e.getMessage()); + } + } + if (ignoreFileManager != null) { + ignoreFileManager.removeListener(ignoreListener); + } + for (IgnoreEntryCard card : cards) { + card.dispose(); + } + if (container != null && !container.isDisposed()) { + container.dispose(); + } + super.dispose(); + } + + /** + * Inner class representing a single row card with strict column alignment. + */ + private static class IgnoreEntryCard { + private final Composite cardComposite; + private final Button checkboxButton; + private final String key; + private final IgnoreEntry entry; + private final Font boldFont; + private boolean isSelected; + + public IgnoreEntryCard(Composite parent, String key, IgnoreEntry entry, DevAssistIgnoredFindings parentView, + boolean initiallySelected) { + this.key = key; + this.entry = entry; + this.isSelected = initiallySelected; + + // Row Container + cardComposite = new Composite(parent, SWT.NONE); + cardComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + + GridLayout layout = new GridLayout(4, false); + layout.marginWidth = 0; + layout.marginHeight = 4; + layout.horizontalSpacing = 16; + cardComposite.setLayout(layout); + + // Column 1: Checkbox + checkboxButton = new Button(cardComposite, SWT.CHECK); + GridData col1Data = new GridData(SWT.LEFT, SWT.TOP, false, false); + checkboxButton.setLayoutData(col1Data); + checkboxButton.setSelection(initiallySelected); + checkboxButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + isSelected = checkboxButton.getSelection(); + parentView.onCardSelectionChanged(key, isSelected); + } + }); + + // Column 2: Content Container + Composite contentComposite = new Composite(cardComposite, SWT.NONE); + GridData col2Data = new GridData(SWT.FILL, SWT.FILL, true, false); + contentComposite.setLayoutData(col2Data); + + GridLayout contentLayout = new GridLayout(1, false); + contentLayout.marginWidth = 0; + contentLayout.marginHeight = 0; + contentLayout.verticalSpacing = 4; + contentComposite.setLayout(contentLayout); + + // Title Line: Icons + Name + Composite titleComposite = new Composite(contentComposite, SWT.NONE); + titleComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + GridLayout titleLayout = new GridLayout(3, false); + titleLayout.marginWidth = 0; + titleLayout.marginHeight = 0; + titleLayout.horizontalSpacing = 6; + titleComposite.setLayout(titleLayout); + + Image cardIcon = IconRegistry.getCardIcon( + entry.getType() != null ? entry.getType().toString() : "VULNERABILITY", + entry.getSeverity() != null ? entry.getSeverity() : "MEDIUM"); + if (cardIcon != null) { + Label cardIconLabel = new Label(titleComposite, SWT.NONE); + cardIconLabel.setImage(cardIcon); + } + + Image severityIcon = IconRegistry.getThemeAwareIcon( + entry.getSeverity() != null ? entry.getSeverity() : "MEDIUM", IconRegistry.Size.MEDIUM); + if (severityIcon != null) { + Label severityIconLabel = new Label(titleComposite, SWT.NONE); + severityIconLabel.setImage(severityIcon); + } + + Label nameLabel = new Label(titleComposite, SWT.NONE); + nameLabel.setText(entry.getPackageName() != null ? entry.getPackageName() : "Unknown Risk"); + nameLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + this.boldFont = new Font(parent.getDisplay(), nameLabel.getFont().getFontData()[0].getName(), + nameLabel.getFont().getFontData()[0].getHeight(), SWT.BOLD); + nameLabel.setFont(boldFont); + + // Description Label + Label descLabel = new Label(contentComposite, SWT.WRAP); + String desc = entry.getDescription() != null && !entry.getDescription().isEmpty() ? entry.getDescription() + : "Description not available"; + descLabel.setText(desc); + descLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + descLabel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + + // Tags Line with Configurable Rounded Badges + if (entry.getFiles() != null && !entry.getFiles().isEmpty()) { + List<IgnoreEntry.FileReference> activeFiles = entry.getFiles().stream() + .filter(IgnoreEntry.FileReference::isActive).collect(Collectors.toList()); + + Composite tagsComposite = new Composite(contentComposite, SWT.NONE); + tagsComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + // +2 reserves columns for the type badge and the expand/collapse link, in + // addition to one column per active file badge. + GridLayout tagsLayout = new GridLayout(activeFiles.size() + 2, false); + tagsLayout.marginWidth = 0; + tagsLayout.marginHeight = 2; + tagsLayout.horizontalSpacing = 6; + tagsComposite.setLayout(tagsLayout); + + Color tagBorderColor = parent.getDisplay().getSystemColor(SWT.COLOR_GRAY); + Color tagTextColor = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + + // Type Badge (Slight rounding: 4px) + createFlatBadge(tagsComposite, entry.getType() != null ? entry.getType().toString() : "VULNERABILITY", + tagBorderColor, tagTextColor, 4, null); + + // File Badges (Pill style rounding: 10px) - clickable, navigates to the + // file (and line, if known), matching the JetBrains plugin's behavior. + int visibleCount = Math.min(2, activeFiles.size()); + for (IgnoreEntry.FileReference file : activeFiles.subList(0, visibleCount)) { + createFileBadge(tagsComposite, file, parentView, tagBorderColor, tagTextColor); + } + + // "N more" / "see less" expand-collapse link for remaining files, matching + // the JetBrains plugin's behavior. + if (activeFiles.size() > visibleCount) { + List<IgnoreEntry.FileReference> hidden = activeFiles.subList(visibleCount, activeFiles.size()); + List<Control> expandedControls = new ArrayList<>(); + + Link expandLink = new Link(tagsComposite, SWT.NONE); + expandLink.setText("<a>+" + hidden.size() + (hidden.size() == 1 ? " more file</a>" : " more files</a>")); + expandLink.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + expandLink.addListener(SWT.Selection, e -> { + ((GridData) expandLink.getLayoutData()).exclude = true; + expandLink.setVisible(false); + + for (IgnoreEntry.FileReference file : hidden) { + expandedControls + .add(createFileBadge(tagsComposite, file, parentView, tagBorderColor, tagTextColor)); + } + + Link collapseLink = new Link(tagsComposite, SWT.NONE); + collapseLink.setText("<a>see less</a>"); + collapseLink.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + collapseLink.addListener(SWT.Selection, e2 -> { + for (Control c : expandedControls) { + c.dispose(); + } + expandedControls.clear(); + ((GridData) expandLink.getLayoutData()).exclude = false; + expandLink.setVisible(true); + refreshTagsLayout(tagsComposite, parentView); + }); + expandedControls.add(collapseLink); + + refreshTagsLayout(tagsComposite, parentView); + }); + } + } + + // Column 3: Dynamic Last Updated Date + Label lastUpdatedLabel = new Label(cardComposite, SWT.NONE); + String relativeDate = DateFormatUtil.formatRelativeDate(entry.getDateAdded()); + lastUpdatedLabel.setText(relativeDate); + lastUpdatedLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + GridData col3Data = new GridData(SWT.RIGHT, SWT.CENTER, false, false); + col3Data.widthHint = 110; + lastUpdatedLabel.setLayoutData(col3Data); + + // Column 4: Single Revive Button + Button reviveButton = new Button(cardComposite, SWT.PUSH); + reviveButton.setText("« Revive"); + GridData col4Data = new GridData(SWT.RIGHT, SWT.CENTER, false, false); + col4Data.widthHint = 95; + reviveButton.setLayoutData(col4Data); + + reviveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + parentView.onCardRevive(entry); + } + }); + } + + public void setSelected(boolean selected) { + this.isSelected = selected; + if (checkboxButton != null && !checkboxButton.isDisposed()) { + checkboxButton.setSelection(selected); + } + } + + public IgnoreEntry getEntry() { + return entry; + } + + public String getKey() { + return key; + } + + public void dispose() { + if (boldFont != null && !boldFont.isDisposed()) { + boldFont.dispose(); + } + if (cardComposite != null && !cardComposite.isDisposed()) { + cardComposite.dispose(); + } + } + } + + /** + * Creates a clickable file badge that navigates to the referenced file (and + * line, if known) when clicked. + */ + private static Composite createFileBadge(Composite parent, IgnoreEntry.FileReference file, + DevAssistIgnoredFindings parentView, Color borderColor, Color textColor) { + String fileName = file.getPath() != null ? Paths.get(file.getPath()).getFileName().toString() : "unknown"; + return createFlatBadge(parent, "📄 " + fileName, borderColor, textColor, 10, + () -> parentView.navigateToFile(file)); + } + + /** + * Re-flows the tags row and its ancestors after an expand/collapse toggle, + * and recomputes the scroll area's min height so the ScrolledComposite + * accounts for the new card height. + */ + private static void refreshTagsLayout(Composite tagsComposite, DevAssistIgnoredFindings parentView) { + for (Composite c = tagsComposite; c != null && !c.isDisposed(); c = c.getParent()) { + c.layout(true, true); + } + parentView.relayoutCards(); + } + + private static Composite createFlatBadge(Composite parent, String text, Color borderColor, Color textColor, + int cornerRadius, Runnable onClick) { + Composite badgeContainer = new Composite(parent, SWT.NONE); + GridLayout containerLayout = new GridLayout(1, false); + containerLayout.marginWidth = 6; + containerLayout.marginHeight = 2; + badgeContainer.setLayout(containerLayout); + + Label label = new Label(badgeContainer, SWT.NONE); + label.setText(text); + label.setForeground(textColor); + label.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); + + badgeContainer.addPaintListener(e -> { + e.gc.setAntialias(SWT.ON); + e.gc.setForeground(borderColor); + + int width = badgeContainer.getBounds().width - 1; + int height = badgeContainer.getBounds().height - 1; + + e.gc.drawRoundRectangle(0, 0, width, height, cornerRadius, cornerRadius); + }); + + label.setBackground(badgeContainer.getBackground()); + + if (onClick != null) { + Cursor handCursor = parent.getDisplay().getSystemCursor(SWT.CURSOR_HAND); + badgeContainer.setCursor(handCursor); + label.setCursor(handCursor); + + MouseAdapter clickListener = new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + onClick.run(); + } + }; + badgeContainer.addMouseListener(clickListener); + label.addMouseListener(clickListener); + } + + return badgeContainer; + } +} \ No newline at end of file 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..801c7b62 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java @@ -0,0 +1,525 @@ +package com.checkmarx.eclipse.devassist.ui.findings.marker; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; + +/** + * 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 { + + private static final String MARKER_TYPE = "com.checkmarx.eclipse.plugin.checkmarxProblemMarker"; + + // Marker attribute names (prefixed with cx. to avoid collision) + private static final String ATTR_ISSUE_ID = "cx.issueId"; + private static final String ATTR_SEVERITY = "cx.severity"; + 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"; + private static final String ATTR_VULNERABILITIES = "cx.vulnerabilities"; + + // Delimiters for the flat vulnerabilities encoding. These control characters + // (unit separator / record separator) can't legally appear in marker text + // (title/description), unlike printable characters such as commas or pipes. + private static final String VULN_FIELD_SEP = ""; + private static final String VULN_RECORD_SEP = ""; + + /** + * Reads the Checkmarx issue id off a marker without needing a full + * fromMarker() reconstruction - used by the hover to cross-reference a + * MarkerAnnotation against an already-rendered FindingsAnnotation for the + * same underlying finding. + */ + public static String getIssueId(IMarker marker) { + return marker.getAttribute(ATTR_ISSUE_ID, ""); + } + + /** + * Reconstruct a ScanIssue from marker attributes. + * 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); + String vulnerabilitiesRaw = marker.getAttribute(ATTR_VULNERABILITIES, ""); + + // 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); + if (!vulnerabilitiesRaw.isEmpty()) { + issue.setVulnerabilities(decodeVulnerabilities(vulnerabilitiesRaw)); + } + + // 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()); + } + + // Carry the full vulnerabilities list (ASCA/IAC can group several + // vulnerabilities under one issue) so marker-based hover/details + // reconstruction doesn't collapse back down to a single entry. + if (issue.getVulnerabilities() != null && !issue.getVulnerabilities().isEmpty()) { + marker.setAttribute(ATTR_VULNERABILITIES, encodeVulnerabilities(issue.getVulnerabilities())); + } + + // Set standard marker attributes from location + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + applyLocationAttributes(marker, issue.getLocations().get(0)); + + // Calculate severity for Eclipse marker system (0=info, 1=warning, 2=error) + int severity = calculateMarkerSeverity(issue.getSeverity()); + marker.setAttribute(IMarker.SEVERITY, severity); + } + } catch (Exception e) { + + e.printStackTrace(); + } + } + + /** + * Ensures a {@value #MARKER_TYPE} marker exists for this finding, creating and + * populating + * one if none does yet. This is what CheckmarxMarkerResolutionGenerator's + * Ctrl+1/quick-fix- + * in-hover actions anchor to; ProblemDecorator calls this for every issue it + * decorates so the + * marker (and therefore the quick-fix actions) exists as soon as the squiggly + * does, instead of + * only after the user navigates to that specific finding from the Findings + * view. + * + * @param file the file the issue was found in + * @param issue the finding to ensure a marker for + */ + public static void ensureMarker(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + + try { + if (findMarker(file, issue) != null) { + return; + } + IMarker marker = file.createMarker(MARKER_TYPE); + int lineNumber = issue.getLocations().get(0).getLine(); + marker.setAttribute(IMarker.LINE_NUMBER, lineNumber > 0 ? lineNumber : 1); + marker.setAttribute(IMarker.MESSAGE, issue.getTitle()); + marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING); + marker.setAttribute(IMarker.USER_EDITABLE, false); + + populateMarker(marker, issue); + } catch (Exception e) { + // Marker creation is best-effort: the squiggly annotation and + // CheckmarxAnnotationHover + // (both driven by the live ScanIssue/FindingsAnnotation, not this marker) still + // work + // even if this fails. + } + } + + /** + * Deletes every {@value #MARKER_TYPE} marker on the given file. + * Called before re-creating markers for the current (post-filter) issue list so + * that findings which were ignored/resolved/filtered out don't leave a stale + * gutter icon and Problems-view entry behind - ensureMarker() only ever adds, + * it never removes markers for issues no longer present. + * + * @param file the file to clear markers from + */ + public static void clearAllMarkers(IFile file) { + if (file == null || !file.exists()) { + return; + } + try { + file.deleteMarkers(MARKER_TYPE, true, IResource.DEPTH_ZERO); + } catch (Exception e) { + // best-effort + } + } + + /** + * Finds the existing {@value #MARKER_TYPE} marker for a ScanIssue, matching by + * the stable + * scanIssueId when available and falling back to line+title for findings + * without one. + * + * @param file the file to search + * @param issue the finding to find a marker for + * @return the matching marker, or null if none exists + */ + public static IMarker findMarker(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return null; + } + + String issueId = issue.getScanIssueId(); + int issueLine = issue.getLocations().get(0).getLine(); + String issueTitle = issue.getTitle(); + + try { + IMarker[] markers = file.findMarkers(MARKER_TYPE, true, IResource.DEPTH_ZERO); + for (IMarker marker : markers) { + if (issueId != null && !issueId.isEmpty()) { + if (issueId.equals(marker.getAttribute(ATTR_ISSUE_ID, ""))) { + return marker; + } + continue; + } + // Fallback for findings without a scanIssueId: line+title heuristic. + int markerLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (markerLine == issueLine) { + String markerMsg = marker.getAttribute(IMarker.MESSAGE, ""); + if (issueTitle == null || issueTitle.isEmpty() || markerMsg.contains(issueTitle)) { + return marker; + } + } + } + } catch (Exception e) { + // fall through + } + + return null; + } + + /** + * Sets IMarker.LINE_NUMBER and, when possible, IMarker.CHAR_START/CHAR_END from + * a + * Location. Most scan engines (OSS, IaC, Secrets, Containers) report + * startIndex/endIndex + * as offsets relative to the start of the line, not the file - writing them + * straight into + * CHAR_START/CHAR_END as absolute file offsets collapses every marker onto + * whichever line + * happens to contain that many characters (almost always line 1), independent + * of which + * line the finding is actually on. This resolves the line's real offset in the + * document and + * adds it in, mirroring the conversion ProblemDecorator already applies when + * positioning the + * squiggly annotation - so the IMarker (which is what Eclipse's built-in + * quick-fix-in-hover + * and Ctrl+1 machinery anchors to) lands on the same line as the squiggly + * instead of drifting + * to a different one. + * + * @param marker the IMarker being populated + * @param location the finding's location (line, and possibly line-relative or + * absolute start/end) + */ + private static void applyLocationAttributes(IMarker marker, Location location) { + try { + marker.setAttribute(IMarker.LINE_NUMBER, location.getLine()); + } catch (Exception e) { + return; + } + + IDocument document = resolveDocument(marker); + if (document == null) { + // No open editor for this file (yet). Leave CHAR_START/CHAR_END unset rather + // than + // writing the scanner's raw, often line-relative, start/end indices in as if + // they + // were absolute file offsets - Eclipse falls back to deriving a position from + // LINE_NUMBER alone, which is still correct for the line even without a precise + // range. + return; + } + + try { + int line = Math.max(0, location.getLine() - 1); + if (line >= document.getNumberOfLines()) { + return; + } + + IRegion lineInfo = document.getLineInformation(line); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + int docLength = document.getLength(); + + boolean isAbsoluteOffset = location.isAbsoluteOffset(); + int charStart = isAbsoluteOffset ? location.getStartIndex() : (lineOffset + location.getStartIndex()); + int charEnd = isAbsoluteOffset ? location.getEndIndex() : (lineOffset + location.getEndIndex()); + + // Scanners that don't report a real column range (e.g. ASCA only sets the line, + // leaving start/end at their default of 0) collapse to the very start of the + // line here - + // expand to the whole (leading-whitespace-trimmed) line instead of leaving a + // zero-length position, which some Eclipse annotation-model paths treat as + // invalid. + if (charStart <= lineOffset) { + charStart = lineOffset + getLeadingWhitespaceOffset(document, lineOffset, lineLength); + } + if (charEnd <= charStart) { + charEnd = lineOffset + lineLength; + } + + charStart = Math.max(0, Math.min(charStart, docLength)); + charEnd = Math.max(charStart, Math.min(charEnd, docLength)); + + marker.setAttribute(IMarker.CHAR_START, charStart); + marker.setAttribute(IMarker.CHAR_END, charEnd); + } catch (Exception e) { + // Leave CHAR_START/CHAR_END unset; the LINE_NUMBER set above still positions + // the + // marker on the correct line. + } + } + + /** + * Finds the document for the marker's own file by searching every open editor + * reference + * across all workbench windows - not just the active editor - so markers + * created for a + * file that isn't currently focused (e.g. background/real-time scan results) + * still resolve + * to the right document instead of silently reading whichever file happens to + * be active. + * Returns null (rather than guessing) if the file has no open editor. + */ + private static IDocument resolveDocument(IMarker marker) { + try { + IResource resource = marker.getResource(); + if (!(resource instanceof IFile)) { + return null; + } + IFile file = (IFile) resource; + + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + + for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) { + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + continue; + } + for (IEditorReference ref : page.getEditorReferences()) { + IEditorPart editorPart = ref.getEditor(false); + if (editorPart == null) { + continue; + } + IEditorInput input = editorPart.getEditorInput(); + if (!(input instanceof IFileEditorInput) + || !file.equals(((IFileEditorInput) input).getFile())) { + continue; + } + ITextEditor textEditor = editorPart.getAdapter(ITextEditor.class); + if (textEditor != null) { + return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + } + } + } catch (Exception e) { + // fall through + } + return null; + } + + private static int getLeadingWhitespaceOffset(IDocument document, int lineOffset, int lineLength) { + try { + String lineText = document.get(lineOffset, lineLength); + int count = 0; + while (count < lineText.length() && Character.isWhitespace(lineText.charAt(count))) { + count++; + } + return count; + } catch (Exception e) { + return 0; + } + } + + /** + * Flattens title/description pairs into one marker-attribute-safe string. + */ + private static String encodeVulnerabilities(List<Vulnerability> vulnerabilities) { + StringBuilder sb = new StringBuilder(); + for (Vulnerability vuln : vulnerabilities) { + if (sb.length() > 0) { + sb.append(VULN_RECORD_SEP); + } + sb.append(sanitize(vuln.getTitle())).append(VULN_FIELD_SEP).append(sanitize(vuln.getDescription())); + } + return sb.toString(); + } + + private static List<Vulnerability> decodeVulnerabilities(String raw) { + List<Vulnerability> result = new ArrayList<>(); + for (String record : raw.split(VULN_RECORD_SEP, -1)) { + if (record.isEmpty()) { + continue; + } + String[] fields = record.split(VULN_FIELD_SEP, -1); + Vulnerability vuln = new Vulnerability(); + vuln.setTitle(fields.length > 0 ? fields[0] : ""); + vuln.setDescription(fields.length > 1 ? fields[1] : ""); + result.add(vuln); + } + return result; + } + + private static String sanitize(String value) { + if (value == null) { + return ""; + } + return value.replace(VULN_FIELD_SEP, " ").replace(VULN_RECORD_SEP, " "); + } + + /** + * Convert Checkmarx severity to Eclipse marker severity level. + */ + private static int calculateMarkerSeverity(String severity) { + if (severity == null) { + return IMarker.SEVERITY_WARNING; + } + + switch (severity.toLowerCase()) { + case "malicious": + 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 MALICIOUS: + 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<ScanIssue> issues; + private final Map<String, Long> problemCount; + private final Image icon; + + public FileNodeLabel(String fileName, String filePath, List<ScanIssue> issues) { + this(fileName, filePath, issues, null, null); + } + + public FileNodeLabel(String fileName, String filePath, List<ScanIssue> issues, Image icon) { + this(fileName, filePath, issues, calculateProblemCount(issues), icon); + } + + public FileNodeLabel(String fileName, String filePath, List<ScanIssue> issues, Map<String, Long> 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<String, Long> calculateProblemCount(List<ScanIssue> issues) { + Map<String, Long> 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<ScanIssue> getIssues() { + return issues; + } + + public Map<String, Long> 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..060c70ac --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java @@ -0,0 +1,37 @@ +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; + private final FileNodeLabel parentNode; + + public ScanDetailWithPath(ScanIssue detail, String filePath, FileNodeLabel parentNode) { + this.detail = detail; + this.filePath = filePath; + this.parentNode = parentNode; + } + + public FileNodeLabel getParentNode() { + return parentNode; + } + + 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..e55605da --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java @@ -0,0 +1,131 @@ +package com.checkmarx.eclipse.devassist.ui.findings.provider; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IEditorRegistry; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; + +/** + * 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 { + + private final Map<ImageDescriptor, Image> imageCache = new HashMap<>(); + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, List<ScanIssue>> map = (Map<String, List<ScanIssue>>) inputElement; + return map.entrySet().stream().map(entry -> { + String fileName = getFileName(entry.getKey()); + Image fileIcon = getFileIcon(fileName); + return new FileNodeLabel(fileName, entry.getKey(), entry.getValue(), fileIcon); + }).toArray(); + } + return new Object[0]; + } + + private Image getFileIcon(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return null; + } + + try { + IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry(); + ImageDescriptor imageDescriptor = registry.getImageDescriptor(fileName); + + if (imageDescriptor != null) { + return imageCache.computeIfAbsent(imageDescriptor, descriptor -> descriptor.createImage()); + } + } catch (Exception e) { + CxLogger.error("Error retrieving file icon for " + fileName, e); + } + + return null; + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) parentElement; + // Sort issues for this file by severity (most severe first) + java.util.List<ScanIssue> sorted = new java.util.ArrayList<>(); + if (fileNode.getIssues() != null) { + sorted.addAll(fileNode.getIssues()); + sorted.sort((a, b) -> Integer.compare(getSeverityRank(a == null ? null : a.getSeverity()), + getSeverityRank(b == null ? null : b.getSeverity()))); + } + + return sorted.stream() + .map(issue -> new ScanDetailWithPath(issue, fileNode.getFilePath(), fileNode)).toArray(); + } + return new Object[0]; + } + + /** + * Map severity string to an integer rank where lower = more severe. + * Unknown/null severities are given a low priority (higher numeric rank). + */ + private int getSeverityRank(String severity) { + return SeverityLevel.fromValue(severity).getPrecedence(); + } + + @Override + public Object getParent(Object element) { + if (element instanceof ScanDetailWithPath) { + return ((ScanDetailWithPath) element).getParentNode(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof FileNodeLabel) { + return !((FileNodeLabel) element).getIssues().isEmpty(); + } + return false; + } + + private String getFileName(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return "Unknown"; + } + int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + if (lastSeparator >= 0) { + return filePath.substring(lastSeparator + 1); + } + return filePath; + } + + @Override + public void dispose() { + // Dispose all cached native OS handles to prevent memory leaks + for (Image image : imageCache.values()) { + if (image != null && !image.isDisposed()) { + image.dispose(); + } + } + imageCache.clear(); + } + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java new file mode 100644 index 00000000..e336fdfc --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java @@ -0,0 +1,172 @@ +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.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Event; + +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; + +/** + * 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 = { "malicious", "critical", "high", "medium", "low" }; + private static final int BETWEEN_BADGE_SPACING = 4; // Space between different shield groups + private static final int TEXT_TO_BADGE_PADDING = 28; // Space after filename before first badge + + 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.getThemeAwareIcon(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<String, Long> 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<String, Long> 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 theme-aware shield icon (light or dark variant based on current theme) + Image badgePng = IconRegistry.getThemeAwareIcon(severity, IconRegistry.Size.MEDIUM); + + if (badgePng != null) { + // Draw Shield Badge + 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); + + // Set text color based on theme and selection state + if ((event.detail & SWT.SELECTED) != 0) { + event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); + } else { + // Use theme-based colors for non-selected state + if (DevAssistUtils.isDarkTheme()) { + event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_WHITE)); + } else { + event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK)); + } + } + + // Make count text bold + Font originalFont = event.gc.getFont(); + FontData[] fontData = originalFont.getFontData(); + for (FontData fd : fontData) { + fd.setStyle(fd.getStyle() | SWT.BOLD); + fd.setHeight(9); + } + org.eclipse.swt.graphics.Font boldFont = new org.eclipse.swt.graphics.Font(event.display, fontData); + event.gc.setFont(boldFont); + + 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<Integer, CheckmarxDocumentListener> activeListeners = new HashMap<>(); +// +// /** +// * Map of documents to their associated scan jobs. +// * Key: IDocument hash code +// * Value: RealTimeScanJob (for cleanup and tracking) +// */ +// private final Map<Integer, RealTimeScanJob> 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<com.checkmarx.eclipse.devassist.model.ScanIssue> 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/resolution/CheckmarxMarkerResolutionGenerator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java new file mode 100644 index 00000000..8cc6d6e2 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java @@ -0,0 +1,29 @@ +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 QuickFixRemediationResolution(marker), + new ViewFindingDetailsResolution(marker), + new CopyDetailsResolution(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/CopyDetailsResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CopyDetailsResolution.java new file mode 100644 index 00000000..5a3c9003 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CopyDetailsResolution.java @@ -0,0 +1,67 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IMarkerResolution2; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; + +/** + * Marker resolution that copies the finding's title and description to the clipboard. + * Implements IMarkerResolution2 for efficient hasResolutions() checks. + */ +public class CopyDetailsResolution implements IMarkerResolution2 { + + private final Image icon; + + public CopyDetailsResolution(IMarker marker) { + this.icon = ResolutionIconHelper.severityIconForMarker(marker); + } + + @Override + public String getLabel() { + return DevAssistConstants.COPY_DETAILS_FIX_NAME; + } + + @Override + public String getDescription() { + return "Copy this finding's title and description to the clipboard"; + } + + @Override + public Image getImage() { + return icon; + } + + @Override + public void run(IMarker marker) { + try { + ScanIssue issue = MarkerIssueMapper.fromMarker(marker); + if (issue == null) { + CxLogger.warning("CopyDetailsResolution: could not reconstruct ScanIssue from marker"); + return; + } + String title = issue.getTitle() != null ? issue.getTitle() : ""; + String description = issue.getDescription() != null ? issue.getDescription() : ""; + String text = title + "\n" + description; + + Display.getDefault().asyncExec(() -> { + Clipboard clipboard = new Clipboard(Display.getDefault()); + try { + clipboard.setContents(new Object[] { text }, new Transfer[] { TextTransfer.getInstance() }); + } finally { + clipboard.dispose(); + } + }); + } catch (Exception e) { + CxLogger.error("CopyDetailsResolution: failed to copy details", e); + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java new file mode 100644 index 00000000..6b1c200b --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/QuickFixRemediationResolution.java @@ -0,0 +1,58 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IMarkerResolution2; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.remediation.RemediationManager; +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; + +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.QUICK_FIX; + +/** + * Marker resolution that applies automated remediation for a Checkmarx finding. + * Mirrors the JetBrains plugin's DevAssistFix (LocalQuickFix) behavior: + * sends a remediation prompt to Copilot, falling back to clipboard copy. + * Implements IMarkerResolution2 for efficient hasResolutions() checks. + */ +public class QuickFixRemediationResolution implements IMarkerResolution2 { + + private final Image icon; + + public QuickFixRemediationResolution(IMarker marker) { + this.icon = ResolutionIconHelper.severityIconForMarker(marker); + } + + @Override + public String getLabel() { + return DevAssistConstants.FIX_WITH_DEV_ASSIST; + } + + @Override + public String getDescription() { + return "Apply an automated fix for this Checkmarx finding"; + } + + @Override + public Image getImage() { + return icon; + } + + @Override + public void run(IMarker marker) { + try { + ScanIssue issue = MarkerIssueMapper.fromMarker(marker); + if (issue == null) { + CxLogger.warning("QuickFixRemediationResolution: could not reconstruct ScanIssue from marker"); + return; + } + new RemediationManager().fixWithCxOneAssist(issue, QUICK_FIX); + } catch (Exception e) { + CxLogger.error("QuickFixRemediationResolution: failed to apply remediation", e); + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java new file mode 100644 index 00000000..6aa75821 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ResolutionIconHelper.java @@ -0,0 +1,40 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.graphics.Image; + +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; + +/** + * Shared helper for IMarkerResolution2 implementations to look up the + * severity icon for a Checkmarx marker, so all 4 Quick Fix actions for a + * given finding show the same severity-colored icon (reusing the existing + * IconRegistry SVG severity icons rather than introducing new action-specific + * icon assets). + */ +final class ResolutionIconHelper { + + private static final String ATTR_SEVERITY = "cx.severity"; + + private ResolutionIconHelper() { + } + + /** + * Reads the marker's stored severity attribute directly (without fully + * reconstructing a ScanIssue) and resolves it to a severity icon. + * + * @param marker the Checkmarx problem marker + * @return the severity Image, or null if unavailable/marker deleted + */ + static Image severityIconForMarker(IMarker marker) { + try { + if (marker == null || !marker.exists()) { + return null; + } + String severity = marker.getAttribute(ATTR_SEVERITY, null); + return severity != null ? IconRegistry.getIcon(severity) : null; + } catch (Exception e) { + return null; + } + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java new file mode 100644 index 00000000..af9742af --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java @@ -0,0 +1,255 @@ +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 { + + private final Image icon; + + public ViewFindingDetailsResolution(IMarker marker) { + this.icon = ResolutionIconHelper.severityIconForMarker(marker); + } + + @Override + public String getLabel() { + return "View Finding Details"; + } + + @Override + public String getDescription() { + return "Open detailed information about this Checkmarx finding"; + } + + @Override + public Image getImage() { + return icon; + } + + @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<String> 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..b80ff112 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/preferences/WelcomeDialog.java @@ -0,0 +1,499 @@ +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 = 620; + 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.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()); + + // Initialize real-time state (first-time login handling) + if (mcpEnabled) { + initializeRealtimeState(); + } + + // 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 initializeRealtimeState() { + if (!mcpEnabled) { + return; + } + + com.checkmarx.eclipse.devassist.state.ScannerStateManager stateManager = + new com.checkmarx.eclipse.devassist.state.ScannerStateManager(); + + // On first login (no user preferences set yet), enable all scanners + if (!stateManager.isUserPreferencesSet()) { + settingsManager.setAll(true); + stateManager.setUserPreferencesSet(true); + } + } + + 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(); + boolean allEnabled = settingsManager.areAllEnabled(); + + realTimeScannersCheckbox.setSelection(anyEnabled); + + // Disable checkbox if any scanner is disabled (user has customized preferences) + boolean hasCustomizedPreferences = !allEnabled && anyEnabled; + realTimeScannersCheckbox.setEnabled(mcpEnabled && !hasCustomizedPreferences); + + 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(); + boolean hasCustomizedPreferences = !allEnabled && anyEnabled; + + String tooltipText; + if (hasCustomizedPreferences) { + tooltipText = "Some scanners are disabled. Manage scanner preferences in Preferences > Checkmarx"; + } else 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/DateFormatUtil.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DateFormatUtil.java new file mode 100644 index 00000000..3e746ab1 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DateFormatUtil.java @@ -0,0 +1,73 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * Utility class for formatting dates as relative time strings. + * Formats dates as "Today", "Yesterday", "X days ago", "X weeks ago", etc. + */ +public final class DateFormatUtil { + + private DateFormatUtil() { + // Private constructor to prevent instantiation + } + + /** + * Formats an ISO 8601 date string as a relative date string. + * Supports both Instant format (e.g., "2026-06-24T17:46:28.459238500Z") + * and ZonedDateTime format (e.g., "2022-08-25T12:35:24.722784400+05:30[Asia/Calcutta]") + * Examples: "Today", "1 day ago", "2 weeks ago", "3 months ago" + * + * @param isoDateString ISO 8601 date format string + */ + public static String formatRelativeDate(String isoDateString) { + if (isoDateString == null || isoDateString.isEmpty()) { + return "Unknown"; + } + + try { + long days = 0; + + // Try parsing as Instant first (new format: "2026-06-24T17:46:28.459238500Z") + if (isoDateString.endsWith("Z")) { + java.time.Instant instant = java.time.Instant.parse(isoDateString); + days = ChronoUnit.DAYS.between(instant.atZone(java.time.ZoneId.systemDefault()).toLocalDate(), + java.time.ZonedDateTime.now().toLocalDate()); + } else { + // Parse as ZonedDateTime (old format: "2022-08-25T12:35:24.722784400+05:30[Asia/Calcutta]") + // Remove the zone ID part in brackets if present + String dateString = isoDateString; + int bracketIndex = isoDateString.indexOf('['); + if (bracketIndex > 0) { + dateString = isoDateString.substring(0, bracketIndex); + } + + ZonedDateTime dateTime = ZonedDateTime.parse(dateString); + days = ChronoUnit.DAYS.between(dateTime.toLocalDate(), ZonedDateTime.now().toLocalDate()); + } + + if (days == 0) { + return "Today"; + } else if (days == 1) { + return "Yesterday"; + } else if (days < 7) { + return days + " days ago"; + } else if (days < 30) { + long weeks = days / 7; + return weeks + (weeks == 1 ? " week ago" : " weeks ago"); + } else if (days < 365) { + long months = days / 30; + return months + (months == 1 ? " month ago" : " months ago"); + } else { + long years = days / 365; + return years + (years == 1 ? " year ago" : " years ago"); + } + } catch (Exception e) { + CxLogger.warning("DateFormatUtil: Failed to parse date: " + isoDateString + " - " + e.getMessage()); + return isoDateString; + } + } +} 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..602c3091 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java @@ -0,0 +1,227 @@ +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<String> 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"; + public static final String COPY_DETAILS_FIX_NAME = "Copy finding details"; + + // Manifest file patterns + public static final List<String> MANIFEST_FILE_PATTERNS = List.of( + // .NET + "**/Directory.Packages.props", + "**/packages.config", + "**/*.csproj", + // Maven + "**/pom.xml", + // npm + "**/package.json", + "**/yarn.lock", + // Bower + "**/bower.json", + // Python + "**/requirement*.txt", + "**/requirements.txt", + "**/constraints.txt", + "**/constraints-*.txt", + "**/pyproject.toml", + "**/poetry.lock", + "**/setup.cfg", + "**/setup.py", + // Go + "**/go.mod", + // Gradle + "**/*.gradle", + "**/*.gradle.kts", + "**/libs.versions.toml", + // SBT + "**/*.sbt", + // iOS CocoaPods + "**/Podfile", + "**/Podfile.lock", + "**/*.podspec", + "**/*.podspec.json", + // iOS Carthage + "**/Cartfile", + "**/Cartfile.private", + "**/Cartfile.resolved", + // Swift Package Manager + "**/Package.swift", + "**/Package@swift-*.swift", + "**/Package.resolved", + // Dart/Flutter + "**/pubspec.yaml", + "**/pubspec.lock", + // Ruby + "**/Gemfile", + "**/Gemfile.lock", + // Perl + "**/cpanfile", + "**/cpanfile.snapshot", + // PHP Composer + "**/composer.json", + "**/composer.lock" + ); + + // Container file patterns + public static final List<String> 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<String> IAC_SUPPORTED_PATTERNS = List.of( + "**/dockerfile", + "**/*.auto.tfvars", + "**/*.terraform.tfvars"); + + public static final List<String> 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<String> CONTAINER_HELM_EXTENSION = List.of("yml", "yaml"); + public static final List<String> 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<String> AI_AGENT_FILES = List.of("/Dummy.txt", "/", "/AIAssistantInput"); + public static final String SEPERATOR = ":"; + public static final String QUICK_FIX = "QUICK_FIX"; + public static final String UNDO = "Undo"; + public static final String MALICIOUS = "malicious"; + public static final String CRITICAL = "critical"; + public static final String HIGH = "high"; + public static final String MEDIUM = "medium"; + public static final String LOW = "low"; + + /******************************** + * 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"; + + /** + * Constant class to hold image paths. + */ + public static final class ImagePaths { + + private ImagePaths() { + throw new UnsupportedOperationException("Cannot instantiate ImagePaths class"); + } + + public static final String DEV_ASSIST_PNG = "/icons/tooltip/cxone_assist.png"; + public static final String CRITICAL_PNG = "/icons/tooltip/critical.png"; + public static final String HIGH_PNG = "/icons/tooltip/high.png"; + public static final String MEDIUM_PNG = "/icons/tooltip/medium.png"; + public static final String LOW_PNG = "/icons/tooltip/low.png"; + public static final String MALICIOUS_PNG = "/icons/tooltip/malicious.png"; + public static final String PACKAGE_PNG = "/icons/tooltip/package.png"; + public static final String CONTAINER_PNG = "/icons/tooltip/container.png"; + + // Vulnerability Severity Count Icons + public static final String CRITICAL_16_PNG = "/icons/tooltip/severity_count/critical.png"; + public static final String HIGH_16_PNG = "/icons/tooltip/severity_count/high.png"; + public static final String MEDIUM_16_PNG = "/icons/tooltip/severity_count/medium.png"; + public static final String LOW_16_PNG = "/icons/tooltip/severity_count/low.png"; + + // DEVASSIST PLUGIN ICONS + public static final String DEVASSIST_BADGE_PNG = "/icons/tooltip/devassist_badge.png"; + } + +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java new file mode 100644 index 00000000..9b783f5e --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -0,0 +1,487 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.Objects; +import org.eclipse.core.resources.IFile; +import org.eclipse.e4.ui.css.swt.theme.ITheme; +import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jgit.annotations.NonNull; +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.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +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; + +/** + * 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 static final String THEME_ENGINE_DISPLAY_KEY = "org.eclipse.e4.ui.css.swt.theme"; + private static final String DARK_THEME_ID_FRAGMENT = "dark"; + + private DevAssistUtils() { + // Private constructor to prevent instantiation + } + + /** + * 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; + } + } + + /** + * Get a Quick fix name for the quick fix action. + * Returns the appropriate fix name based on the plugin context. + * For Eclipse, defaults to DEV_ASSIST as this plugin is the DevAssist variant. + * + * @return Quick fix name string + */ + public static String getAssistQuickFixName() { + return DevAssistConstants.FIX_WITH_DEV_ASSIST; + } + + /** + * Returns a resource URL string suitable for embedding in an <img src='...'> + * tag + * for the given simple icon key (e.g. "critical", "high", "package", + * "malicious"). + * + * @param iconPath severity or logical icon path + * @return external form URL or empty string if not found + */ + public static String themeBasedPNGIconForHtmlImage(String iconPath) { + if (iconPath == null || iconPath.isEmpty()) { + return ""; + } + boolean dark = isDarkTheme(); + String candidate = iconPath; + if (dark) { + int extensionIndex = iconPath.lastIndexOf(".png"); + if (extensionIndex >= 0) { + candidate = iconPath.substring(0, extensionIndex) + "_dark" + iconPath.substring(extensionIndex); + } else { + candidate = iconPath + "_dark"; + } + } + URL res = DevAssistUtils.class.getResource(candidate); + if (res == null && dark) { + // Fallback to the light variant + candidate = iconPath; + res = DevAssistUtils.class.getResource(candidate); + } + return res != null ? res.toExternalForm() : ""; + } + + /** + * Reads Eclipse's own e4 CSS theme engine - the same mechanism the Platform + * uses to decide dark vs. light styling - so the scanner image always matches + * whatever theme Eclipse is actually rendering with, instead of guessing from + * a color sample (which broke down in practice, e.g. custom/high-contrast + * themes). + */ + public static boolean isDarkTheme() { + ITheme activeTheme = getActiveTheme(); + if (activeTheme != null && activeTheme.getId() != null) { + return activeTheme.getId().toLowerCase().contains(DARK_THEME_ID_FRAGMENT); + } + return isDarkByBackgroundLuminance(); + } + + /** + * Returns the CLI-facing ignore file path (the flattened + * ".checkmarxIgnoredTempList.json") for the given project, so realtime + * scanners can pass it to the CxWrapper scan methods and exclude + * already-ignored findings from scan results. + * + * @param project Project whose ignore file path should be resolved + * @return Absolute path to the temp ignore list, or "" if unavailable + */ + public static String getIgnoreFilePath(org.eclipse.core.resources.IProject project) { + if (project == null) { + return ""; + } + try { + return com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager.getInstance(project) + .getTempListPath().toString(); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to resolve ignore file path: " + e.getMessage()); + return ""; + } + } + + private static ITheme getActiveTheme() { + try { + Display display = Display.getCurrent(); + Object engineData = display != null ? display.getData(THEME_ENGINE_DISPLAY_KEY) : null; + if (engineData instanceof IThemeEngine) { + return ((IThemeEngine) engineData).getActiveTheme(); + } + } catch (Throwable t) { + // e4 CSS theming bundle not present/active in this runtime; caller falls back. + CxLogger.error("Eclipse e4 theme engine unavailable, falling back to color heuristic", + t instanceof Exception ? (Exception) t : new Exception(t)); + } + return null; + } + + /** + * Fallback for the rare runtime where the e4 CSS theme engine isn't registered + * on the Display: approximate dark mode from the widget background luminance. + */ + private static boolean isDarkByBackgroundLuminance() { + Display display = Display.getCurrent(); + if (display == null) { + display = Display.getDefault(); + } + if (display == null) { + return false; + } + Color background = display.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; + } + + /** + * Returns the container tool configured in the global settings. + * @return + */ + public static String getContainerTool() { + try { + // Prefer the typed preference store access which returns the stored value + // or an empty string if not present. Fall back to the generic getPref + // only if needed. Always return a sensible default when empty/null. + String value = Preferences.STORE.getString(Preferences.PREF_CONTAINERS_TOOL); + if (value == null || value.isBlank()) { + // Try the legacy getter which may consult the preference service + value = Preferences.getPref(Preferences.PREF_CONTAINERS_TOOL); + } + return (value == null || value.isBlank()) ? "docker" : value; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error retrieving container tool preference: " + e.getMessage(), e); + return "docker"; // default to docker if preference retrieval fails + } + } +} 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. + * <p> + * This is a utility class and is not meant to be instantiated. + * <p> + * 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/HtmlEscapeUtil.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/HtmlEscapeUtil.java new file mode 100644 index 00000000..34876abd --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/HtmlEscapeUtil.java @@ -0,0 +1,19 @@ +package com.checkmarx.eclipse.devassist.utils; + +public final class HtmlEscapeUtil { + + private HtmlEscapeUtil() { + } + + public static String escape(String text) { + if (text == null) { + return ""; + } + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java new file mode 100644 index 00000000..23bb99c3 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java @@ -0,0 +1,245 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.util.List; + +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * The PackageManager enum represents various package managers used in software development. + * Each constant corresponds to a specific package manager, and the enum provides utility methods for mapping and checking supported package managers. + */ +public enum PackageManager { + + DOTNET("dotnet", ManifestFilePattern.DOTNET), + GRADLE("gradle", ManifestFilePattern.GRADLE), + MAVEN("mvn", ManifestFilePattern.MAVEN), + SBT("sbt", ManifestFilePattern.SBT), + NPM("npm", ManifestFilePattern.NPM), + GO("go", ManifestFilePattern.GO), + PYTHON("python", ManifestFilePattern.PYTHON), + BOWER("bower", ManifestFilePattern.BOWER), + //COCOAPODS("cocoapods", ManifestFilePattern.COCOAPODS), + //CARTHAGE("carthage", ManifestFilePattern.CARTHAGE), + //SWIFT("swift", ManifestFilePattern.SWIFT), + //DART("dart", ManifestFilePattern.DART), + RUBY("ruby", ManifestFilePattern.RUBY), + PHP("php", ManifestFilePattern.PHP), + UNKNOWN("unknown", null); + + private String packageManager; + private ManifestFilePattern manifestPattern; + + PackageManager(String packageManager, ManifestFilePattern pattern) { + this.packageManager = packageManager; + this.manifestPattern = pattern; + } + + public String getPackageManager() { + return packageManager; + } + + public ManifestFilePattern getManifestPattern() { + return manifestPattern; + } + + /** + * Maps a string representation of a package manager to its corresponding PackageManager enum constant. + * @param packageManager + * @return + */ + public static PackageManager fromString(String packageManager) { + for (PackageManager pm : PackageManager.values()) { + if (pm.packageManager.equalsIgnoreCase(packageManager)) { + return pm; + } + } + return UNKNOWN; + } + + /** + * Checks if a given string representation of a package manager is supported by the system. + * @param packageManager + * @return + */ + public static boolean isSupportedPackageManager(String packageManager) { + for (PackageManager pm : PackageManager.values()) { + if (pm.packageManager.equalsIgnoreCase(packageManager)) { + return true; + } + } + return false; + } + + /** + * Maps a given package manager to its corresponding remediation format. + * For example, Gradle and SBT are mapped to Maven, while CocoaPods and Carthage are mapped to Swift. + * If the package manager is not recognized or is null/empty, it returns the original input. + * + * @param packageManager The string representation of the package manager to be mapped. + * @return The corresponding remediation format for the given package manager. + */ + public static String mapToRemediationFormat(String packageManager) { + if (packageManager == null || packageManager.isEmpty()) { + CxLogger.warning("[PACKAGE-MANAGER] Package manager is null or empty, returning as is."); + return packageManager; + } + PackageManager pm = fromString(packageManager.toLowerCase()); + + switch (pm) { + case GRADLE: + case SBT: + return MAVEN.getPackageManager(); + //case COCOAPODS: + //case CARTHAGE: + //return SWIFT.getPackageManager(); + default: + return packageManager; + } + } + + + /** + * Infers companion lock file names based on the manifest file name. + * Some manifests may have multiple companion files (e.g., package.json has both package-lock.json and yarn.lock). + * + * @param fileName name of the manifest file + * @return list of companion file names; empty list if no companions are defined + */ + public static List<String> getCompanionFileNames(String fileName) { + // npm/Yarn - support both package-lock.json (npm) and yarn.lock (yarn) + if (fileName.equals("package.json")) { + return List.of(CompanionFileType.PACKAGE_LOCK_JSON.getCompFileName(), CompanionFileType.YARN_LOCK.getCompFileName()); + } + + // .NET + if (fileName.contains(".csproj")) { + return getCompanionFileNamesByType(CompanionFileType.PACKAGES_LOCK_JSON); + } + + // Swift Package Manager (AST-165765) + if (fileName.equals("Package.swift")) { + return getCompanionFileNamesByType(CompanionFileType.PACKAGE_RESOLVED); + } + if (fileName.startsWith("Package@swift-") && fileName.endsWith(".swift")) { + return List.of(fileName.replace(".swift", ".resolved")); + } + + // CocoaPods (AST-165761) + if (fileName.equals("Podfile")) { + return getCompanionFileNamesByType(CompanionFileType.PODFILE_LOCK); + } + + // Carthage + if (fileName.equals("Cartfile") || fileName.equals("Cartfile.private")) { + return getCompanionFileNamesByType(CompanionFileType.CARTFILE_RESOLVED); + } + + // Ruby Bundler + if (fileName.equals("Gemfile")) { + return getCompanionFileNamesByType(CompanionFileType.GEMFILE_LOCK); + } + + // PHP Composer + if (fileName.equals("composer.json")) { + return getCompanionFileNamesByType(CompanionFileType.COMPOSER_LOCK); + } + + // Python Poetry + if (fileName.equals("pyproject.toml")) { + return getCompanionFileNamesByType(CompanionFileType.POETRY_LOCK); + } + + // Dart/Flutter Pub + if (fileName.equals("pubspec.yaml")) { + return getCompanionFileNamesByType(CompanionFileType.PUBSPEC_LOCK); + } + return List.of(); + } + + /** + * Enum representing companion lock file types for various package managers. + */ + static enum CompanionFileType { + + PACKAGE_LOCK_JSON("package-lock.json"), + YARN_LOCK("yarn.lock"), + PACKAGES_LOCK_JSON("packages.lock.json"), + PACKAGE_RESOLVED("Package.resolved"), + PODFILE_LOCK("Podfile.lock"), + CARTFILE_RESOLVED("Cartfile.resolved"), + GEMFILE_LOCK("Gemfile.lock"), + COMPOSER_LOCK("composer.lock"), + POETRY_LOCK("poetry.lock"), + PUBSPEC_LOCK("pubspec.lock"); + + private String compFileName; + + CompanionFileType(String compFileName) { + this.compFileName = compFileName; + } + + public String getCompFileName() { + return compFileName; + } + } + + public static List<String> getCompanionFileNamesByType(CompanionFileType type) { + if (type == null) { + return List.of(); + } + return List.of(type.getCompFileName()); + } + + /** + * Enum representing manifest file patterns for various package managers. + * Each constant corresponds to a specific package manager and holds a list of file patterns used to identify manifest files. + */ + static enum ManifestFilePattern { + + DOTNET(List.of("**/Directory.Packages.props", "**/packages.config","**/*.csproj")), + GRADLE(List.of("**/*.gradle", "**/*.gradle.kts", "**/libs.versions.toml")), + MAVEN(List.of("**/pom.xml")), + SBT(List.of("**/*.sbt")), + NPM(List.of("**/package.json")), + GO(List.of("**/go.mod")), + PYTHON(List.of("**/requirement*.txt", "**/constraints.txt", "**/constraints-*.txt", "**/pyproject.toml", + "**/setup.cfg", "**/setup.py")), + BOWER(List.of("**/bower.json")), + //COCOAPODS(List.of("**/Podfile", "**/*.podspec", "**/*.podspec.json")), + //CARTHAGE(List.of("**/Cartfile", "**/Cartfile.private")), + //SWIFT(List.of("**/Package.swift", "**/Package@swift-*.swift")), + //DART(List.of("**/pubspec.yaml")), + RUBY(List.of("**/Gemfile")), + PHP(List.of("**/composer.json")); + + private List<String> patterns; + + ManifestFilePattern(List<String> filePatterns) { + this.patterns = filePatterns; + } + + public List<String> getPatterns() { + return patterns; + } + } + + /** + * Retrieves all defined manifest file patterns across all package managers. + * If a package manager has no defined patterns, a warning is logged. + * + * @return a list of all manifest file patterns + */ + public static List<String> getAllPatterns() { + + List<String> allPatterns = new java.util.ArrayList<>(); + + for (ManifestFilePattern pattern : ManifestFilePattern.values()) { + if (pattern.getPatterns() == null || pattern.getPatterns().isEmpty()) { + CxLogger.warning("[PACKAGE-MANAGER] ManifestFilePattern " + pattern.name() + " has no defined patterns."); + } + allPatterns.addAll(pattern.getPatterns()); + } + return allPatterns; + } + +} 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..a21f9cf4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,8 @@ <modules> + <module>common-lib</module> + <module>devassist-lib</module> <module>checkmarx-ast-eclipse-plugin</module> <module>com.checkmarx.eclipse.feature</module> <module>com.checkmarx.eclipse.site</module> @@ -118,7 +120,7 @@ ${project.artifactId}_${unqualifiedVersion}.${buildQualifier} </finalName> </configuration> - </execution> + </execution> </executions> <!-- <configuration> <format>'v'yyyyMMddHHmm</format> </configuration> --> @@ -130,7 +132,7 @@ <version>${tycho.version}</version> <configuration> <executionEnvironment>JavaSE-17</executionEnvironment> - <pomDependencies>ignore</pomDependencies> + <pomDependencies>consider</pomDependencies> <environments> <environment> <os>linux</os> @@ -149,6 +151,34 @@ </environment> </environments> + <extraRequirements> + <requirement> + <type>eclipse-plugin</type> + <id>org.objectweb.asm</id> + <versionRange>[9.10.1,9.10.1]</versionRange> + </requirement> + <requirement> + <type>eclipse-plugin</type> + <id>org.objectweb.asm.tree</id> + <versionRange>[9.10.1,9.10.1]</versionRange> + </requirement> + <requirement> + <type>eclipse-plugin</type> + <id>org.objectweb.asm.commons</id> + <versionRange>[9.10.1,9.10.1]</versionRange> + </requirement> + <requirement> + <type>eclipse-plugin</type> + <id>org.objectweb.asm.util</id> + <versionRange>[9.10.1,9.10.1]</versionRange> + </requirement> + <requirement> + <type>eclipse-plugin</type> + <id>org.objectweb.asm.analysis</id> + <versionRange>[9.10.1,9.10.1]</versionRange> + </requirement> + </extraRequirements> + </configuration> </plugin> <plugin> @@ -201,12 +231,6 @@ <layout>p2</layout> <url>https://download.eclipse.org/technology/swtbot/releases/latest</url> </repository> - <repository> - <id>orbit-4.32</id> - <layout>p2</layout> - <url> - https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.32.0</url> - </repository> <repository> <id>mylyn</id> <layout>p2</layout>