From 4c5b39b0b1ead4ca4621c39bc583e416ca2265e6 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Mon, 3 Aug 2026 11:53:59 +0530 Subject: [PATCH 1/9] Add scan detection backend and Findings window UI Extracted from other/scan-backend: the DevAssist scanner infrastructure (ASCA/OSS/IaC/Secrets/Containers scanners, project lifecycle detection, real-time editor scanning, problem/marker pipeline) and the Checkmarx Findings + Ignored Problems views that display detected issues, including gutter/underline editor annotations and the AI-Assist remediation action. Excludes the MCP-injection feature (devassist/configuration) and unrelated work from that branch (welcome dialog, promotional/preferences UI, login validation, dark theme). Also fixes a pre-existing build.properties/lib jackson version mismatch that blocked packaging from main. Co-Authored-By: Claude Sonnet 5 --- checkmarx-ast-eclipse-plugin-tests/pom.xml | 29 +- .../META-INF/MANIFEST.MF | 5 + checkmarx-ast-eclipse-plugin/build.properties | 4 +- .../icons/cx-one-assist-cube.png | Bin 0 -> 146450 bytes .../icons/severity/critical.svg | 11 + .../icons/severity/critical_16.svg | 4 + .../icons/severity/critical_16_dark.svg | 4 + .../icons/severity/critical_20.svg | 4 + .../icons/severity/critical_20_dark.svg | 4 + .../icons/severity/critical_dark.svg | 4 + .../icons/severity/high.svg | 5 + .../icons/severity/high_16.svg | 4 + .../icons/severity/high_16_dark.svg | 4 + .../icons/severity/high_20.svg | 4 + .../icons/severity/high_20_dark.svg | 4 + .../icons/severity/high_dark.svg | 4 + .../icons/severity/ignored.svg | 4 + .../icons/severity/ignored_16.svg | 4 + .../icons/severity/ignored_16_dark.svg | 4 + .../icons/severity/ignored_20.svg | 4 + .../icons/severity/ignored_20_dark.svg | 4 + .../icons/severity/ignored_24.svg | 4 + .../icons/severity/ignored_24_dark.svg | 4 + .../icons/severity/ignored_dark.svg | 4 + .../icons/severity/low.svg | 4 + .../icons/severity/low_16.svg | 4 + .../icons/severity/low_16_dark.svg | 4 + .../icons/severity/low_20.svg | 4 + .../icons/severity/low_20_dark.svg | 4 + .../icons/severity/low_dark.svg | 4 + .../icons/severity/malicious.svg | 4 + .../icons/severity/malicious_16.svg | 4 + .../icons/severity/malicious_16_dark.svg | 4 + .../icons/severity/malicious_20.svg | 4 + .../icons/severity/malicious_20_dark.svg | 4 + .../icons/severity/medium.svg | 5 + .../icons/severity/medium_16.svg | 4 + .../icons/severity/medium_16_dark.svg | 4 + .../icons/severity/medium_20.svg | 4 + .../icons/severity/medium_20_dark.svg | 4 + .../icons/severity/medium_dark.svg | 4 + .../icons/severity/ok.svg | 4 + .../icons/severity/ok_16.svg | 4 + .../icons/severity/ok_16_dark.svg | 4 + .../icons/severity/ok_20.svg | 4 + .../icons/severity/ok_20_dark.svg | 4 + .../icons/severity/ok_24.svg | 4 + .../icons/severity/ok_24_dark.svg | 4 + .../icons/severity/ok_dark.svg | 4 + .../icons/severity/unknown.svg | 5 + .../icons/severity/unknown_16.svg | 5 + .../icons/severity/unknown_16_dark.svg | 5 + .../icons/severity/unknown_20.svg | 5 + .../icons/severity/unknown_20_dark.svg | 5 + .../icons/severity/unknown_dark.svg | 5 + checkmarx-ast-eclipse-plugin/plugin.xml | 157 ++ .../eclipse/devassist/backend/Constants.java | 21 + .../backend/DevAssistScanStateHolder.java | 178 ++ .../devassist/backend/DevAssistUtils.java | 179 ++ .../backend/GlobalScannerController.java | 310 ++++ .../devassist/backend/ScannerRegistry.java | 328 ++++ .../devassist/backend/SeverityLevel.java | 51 + .../listener/ProjectLifecycleListener.java | 313 ++++ .../backend/result/ResultPublisher.java | 256 +++ .../basescanner/BaseScannerService.java | 150 ++ .../devassist/basescanner/ScanManager.java | 182 ++ .../devassist/basescanner/ScannerService.java | 73 + .../eclipse/devassist/common/ScanResult.java | 32 + .../devassist/common/ScannerFactory.java | 189 ++ .../devassist/factory/CxWrapperFactory.java | 49 + .../inspection/DevAssistInspection.java | 45 + .../inspection/DevAssistInspectionMgr.java | 334 ++++ .../inspection/DevAssistScanScheduler.java | 188 ++ .../eclipse/devassist/model/Location.java | 45 + .../eclipse/devassist/model/ScanEngine.java | 36 + .../eclipse/devassist/model/ScanIssue.java | 194 ++ .../devassist/model/Vulnerability.java | 82 + .../prefs/CheckmarxPreferencePage.java | 214 +++ .../devassist/problems/ProblemBuilder.java | 103 ++ .../devassist/problems/ProblemDecorator.java | 641 +++++++ .../devassist/problems/ProblemDescriptor.java | 117 ++ .../devassist/problems/ProblemHelper.java | 174 ++ .../problems/ProblemHolderService.java | 262 +++ .../problems/ScanIssueProcessor.java | 221 +++ .../scanners/asca/AscaScanResultAdaptor.java | 272 +++ .../scanners/asca/AscaScannerCommand.java | 39 + .../scanners/asca/AscaScannerService.java | 364 ++++ .../ContainerScanResultAdaptor.java | 175 ++ .../containers/ContainerScannerCommand.java | 105 ++ .../containers/ContainerScannerService.java | 288 +++ .../scanners/iac/IacScanResultAdaptor.java | 254 +++ .../scanners/iac/IacScannerCommand.java | 73 + .../scanners/iac/IacScannerService.java | 313 ++++ .../scanners/oss/OssScanResultAdaptor.java | 196 +++ .../scanners/oss/OssScannerCommand.java | 190 ++ .../scanners/oss/OssScannerService.java | 358 ++++ .../secrets/SecretsScanResultAdaptor.java | 161 ++ .../secrets/SecretsScannerCommand.java | 38 + .../secrets/SecretsScannerService.java | 291 +++ .../devassist/state/ScanFrequency.java | 36 + .../eclipse/devassist/state/ScannerState.java | 46 + .../devassist/state/ScannerStateManager.java | 73 + .../devassist/ui/findings/CxFindingsView.java | 1568 +++++++++++++++++ .../actions/VulnerabilityFilterAction.java | 80 + .../actions/VulnerabilityFilterState.java | 67 + .../findings/dialogs/ProblemDescription.java | 181 ++ .../ui/findings/editor/CxFindingsHover.java | 194 ++ .../editor/CxFindingsHoverControl.java | 254 +++ .../editor/CxFindingsInformationControl.java | 47 + .../findings/editor/FindingsAnnotation.java | 71 + .../editor/FindingsEditorOverlay.java | 136 ++ .../editor/FindingsHoverProvider.java | 321 ++++ .../ui/findings/icons/IconRegistry.java | 136 ++ .../findings/icons/SeverityImageComposer.java | 255 +++ .../ignored/CxIgnoredProblemsView.java | 269 +++ .../IgnoredProblemsContentProvider.java | 78 + .../ignored/IgnoredProblemsLabelProvider.java | 76 + .../ignored/IgnoredProblemsStore.java | 212 +++ .../integration/CopilotIntegration.java | 381 ++++ .../integration/RemediationPromptBuilder.java | 274 +++ .../ui/findings/marker/MarkerIssueMapper.java | 196 +++ .../ui/findings/model/FileNodeLabel.java | 82 + .../ui/findings/model/ScanDetailWithPath.java | 31 + .../provider/FindingsContentProvider.java | 142 ++ .../provider/FindingsLabelProvider.java | 161 ++ .../realtime/CheckmarxDocumentListener.java | 89 + .../realtime/CheckmarxEditorListener.java | 408 +++++ .../realtime/FindingsEditorHoverListener.java | 92 + .../ui/findings/realtime/RealTimeScanJob.java | 244 +++ .../CheckmarxMarkerResolutionGenerator.java | 26 + .../ViewFindingDetailsResolution.java | 258 +++ .../ui/findings/utils/FindingsUtils.java | 94 + .../eclipse/startup/PluginStartup.java | 51 +- .../eclipse/utils/PluginConstants.java | 1 + pom.xml | 3 + 135 files changed, 15101 insertions(+), 30 deletions(-) create mode 100644 checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java diff --git a/checkmarx-ast-eclipse-plugin-tests/pom.xml b/checkmarx-ast-eclipse-plugin-tests/pom.xml index 94b4d65c..95ccb716 100644 --- a/checkmarx-ast-eclipse-plugin-tests/pom.xml +++ b/checkmarx-ast-eclipse-plugin-tests/pom.xml @@ -43,35 +43,10 @@ XML CSV - HTML - - check - verify - check - - ${project.build.directory}/jacoco.exec - ${project.basedir}/../checkmarx-ast-eclipse-plugin/target/classes - - org/eclipse/wb/swt/SWTResourceManager.class - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 0.30 - - - - - - - + org.eclipse.tycho @@ -81,7 +56,7 @@ true false junit5 - true + false ${tycho.testArgLine} ${test.includes} diff --git a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF index f9af0477..7d2105c1 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -5,9 +5,14 @@ Bundle-SymbolicName: com.checkmarx.eclipse.plugin;singleton:=true Bundle-Version: 1.0.0.qualifier Bundle-Vendor: Checkmarx Require-Bundle: org.eclipse.ui, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors, org.eclipse.core.runtime, org.eclipse.jdt.core, org.eclipse.ui.ide, + org.eclipse.jface.text, + org.eclipse.text, + org.eclipse.jdt.ui, org.eclipse.jgit, org.eclipse.e4.core.services, com.google.guava, diff --git a/checkmarx-ast-eclipse-plugin/build.properties b/checkmarx-ast-eclipse-plugin/build.properties index 680259c9..75590c4f 100644 --- a/checkmarx-ast-eclipse-plugin/build.properties +++ b/checkmarx-ast-eclipse-plugin/build.properties @@ -6,11 +6,11 @@ bin.includes = plugin.xml,\ lib/slf4j-reload4j-2.0.17.jar,\ lib/slf4j-api-2.0.17.jar,\ lib/jackson-annotations-2.21.jar,\ - lib/jackson-core-2.21.1.jar,\ + lib/jackson-core-2.21.4.jar,\ lib/commons-lang3-3.18.0.jar,\ lib/ast-cli-java-wrapper-2.4.24.jar,\ lib/org.eclipse.mylyn.commons.ui_4.9.0.v20251121-0615.jar,\ - lib/jackson-databind-2.21.1.jar,\ + lib/jackson-databind-2.21.5.jar,\ .,\ lib/org-eclipse-mylyn-commons-core.jar source.. = src/ diff --git a/checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png b/checkmarx-ast-eclipse-plugin/icons/cx-one-assist-cube.png new file mode 100644 index 0000000000000000000000000000000000000000..e48df9b81ab73017718fc2d2a5d36fc9068f73fe GIT binary patch literal 146450 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⪻e7GI(wZ>O2P~$^i^%A1VSqx)d7!Ibe^-ctZZs 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�wW#ux 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 literal 0 HcmV?d00001 diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg new file mode 100644 index 00000000..f3ab95d7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg new file mode 100644 index 00000000..6e1929e8 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg new file mode 100644 index 00000000..9c89888d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg new file mode 100644 index 00000000..9f6ad62b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high.svg new file mode 100644 index 00000000..3b3399b7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg new file mode 100644 index 00000000..4c815e84 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg new file mode 100644 index 00000000..d9b8a81f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg new file mode 100644 index 00000000..50b139fa --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg new file mode 100644 index 00000000..4ec04da0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg new file mode 100644 index 00000000..20246d56 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_24_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low.svg new file mode 100644 index 00000000..a429fd46 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg new file mode 100644 index 00000000..40b203e4 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg new file mode 100644 index 00000000..69f9b3a6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg new file mode 100644 index 00000000..5cb507fb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg new file mode 100644 index 00000000..db43abd1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/malicious_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg new file mode 100644 index 00000000..a004f117 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg new file mode 100644 index 00000000..3a6cda49 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg new file mode 100644 index 00000000..5be2c823 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg new file mode 100644 index 00000000..6cc09bf7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_16_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg new file mode 100644 index 00000000..dc746080 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg new file mode 100644 index 00000000..c139bab4 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_24_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_16_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_20_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/unknown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/checkmarx-ast-eclipse-plugin/plugin.xml b/checkmarx-ast-eclipse-plugin/plugin.xml index e0f787b1..676e436b 100644 --- a/checkmarx-ast-eclipse-plugin/plugin.xml +++ b/checkmarx-ast-eclipse-plugin/plugin.xml @@ -9,6 +9,11 @@ id="com.checkmarx.eclipse.properties.preferencespage" name="Checkmarx One"> + + @@ -21,6 +26,24 @@ name="Checkmarx One Scan" restorable="true"> + + + + @@ -36,6 +59,18 @@ relationship="right" ratio="0.5"> + + + + @@ -43,4 +78,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java new file mode 100644 index 00000000..9cbe8d4b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/Constants.java @@ -0,0 +1,21 @@ +package com.checkmarx.eclipse.devassist.backend; + +/** + * Constants for DevAssist backend operations. + * Mirrors JetBrains Constants pattern. + */ +public class Constants { + // Severity level string constants + public static final String MALICIOUS_SEVERITY = "Malicious"; + public static final String CRITICAL_SEVERITY = "Critical"; + public static final String HIGH_SEVERITY = "High"; + public static final String MEDIUM_SEVERITY = "Medium"; + public static final String LOW_SEVERITY = "Low"; + public static final String OK = "OK"; + public static final String UNKNOWN = "Unknown"; + public static final String IGNORE_LABEL = "Ignored"; + + private Constants() { + // Private constructor to prevent instantiation + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java new file mode 100644 index 00000000..567fc887 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java @@ -0,0 +1,178 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.concurrent.ConcurrentHashMap; + +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Tracks file modification state to prevent redundant scans. + * + * Stores a composite "state hash" for each file: + * - Document modification timestamp + * - File system last-modified time + * - Editor content hash + * + * When a file is requested for scanning, we compare the current state + * with the cached state. If unchanged, we skip the scan and return cached results. + * + * This mirrors the JetBrains DevAssistScanStateHolder pattern. + */ +public class DevAssistScanStateHolder { + + private static final String LOG_TAG = "[SCAN-STATE]"; + + private final ConcurrentHashMap fileStateHash = new ConcurrentHashMap<>(); + + /** + * 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. + * + * @param filePath Absolute file path + * @param currentStateHash Current state of the file + * @return true if file changed (or never scanned), false if unchanged + */ + 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); + return true; + } + + // Compare hashes + boolean changed = !cachedHash.equals(currentStateHash); + if (!changed) { + CxLogger.info(LOG_TAG + " File unchanged (cached): " + filePath); + } + + return changed; + } + + /** + * Clear state for a specific file (e.g., when file is deleted). + * + * @param filePath Absolute file path + */ + public void clearFileState(String filePath) { + if (filePath == null) { + return; + } + + fileStateHash.remove(filePath); + CxLogger.info(LOG_TAG + " Cleared state for: " + filePath); + } + + /** + * Clear all state (on project close). + */ + public void clearAll() { + fileStateHash.clear(); + CxLogger.info(LOG_TAG + " All state cleared"); + } + + /** + * Compute a state hash for a file based on: + * - File system last modified time + * - Document modification timestamp (if open in editor with unsaved changes) + * + * When a file is edited in Eclipse but not saved to disk, the file system + * timestamp doesn't change. This method detects unsaved changes by checking + * if the editor's dirty flag is set, and includes that in the hash. + * + * @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), include a dynamic component to detect changes + boolean hasUnsavedChanges = false; + 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()) { + // Check if this editor is for our file + try { + String editorPath = editor.getEditorInput().getAdapter(org.eclipse.core.resources.IFile.class) + .getLocation().toOSString(); + if (editorPath.equals(filePath)) { + hasUnsavedChanges = true; + break; + } + } catch (Exception e2) { + // Skip if we can't get editor path + } + } + } + if (hasUnsavedChanges) break; + } + if (hasUnsavedChanges) break; + } + } + } catch (Exception e) { + // If workbench check fails, just use file timestamp + hasUnsavedChanges = false; + } + + // If file has unsaved changes, use current time to force re-scan + // This ensures edits are detected even if not yet saved to disk + if (hasUnsavedChanges) { + return System.nanoTime(); // Force different hash on every check while dirty + } + + return fileModified; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error computing state hash: " + e.getMessage()); + return System.currentTimeMillis(); + } + } + + /** + * Get statistics about tracked files. + * + * @return Summary string + */ + public String getStatistics() { + return "Tracked files: " + fileStateHash.size(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java new file mode 100644 index 00000000..9650f308 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java @@ -0,0 +1,179 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; + +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Utility class for DevAssist backend operations. Mirrors JetBrains + * DevAssistUtils pattern. + */ +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"; + public static final List CONTAINER_HELM_EXTENSION = List.of("yml", + "yaml"); + 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) { + // Concatenate components with delimiter for clarity + 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). + * Converts uppercase/lowercase/mixed case to proper format. Examples: "MEDIUM" + * → "Medium", "low" → "Low", "Critical" → "Critical" + * + * @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 as-is if not recognized, will be treated as UNKNOWN in icon lookup + return severity; + } + } + + /** + * Check if severity represents a problem (displayable finding). Returns false + * for OK, UNKNOWN, and IGNORED severities. Mirrors JetBrains implementation for + * UI filtering. + * + * @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. Looks for + * "docker-compose" in the filename (case-insensitive). + * + * @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"); + } + + public static boolean isDockerFile(@NonNull String filePath) { + return Paths.get(filePath).getFileName().toString().toLowerCase().contains("dockerfile"); + } + + public static boolean isYamlFile(String filePath) { + if (Objects.isNull(filePath) || filePath.isBlank()) { + return false; + } + String fileExtension = DevAssistUtils.getFileExtension(filePath); + return Objects.nonNull(fileExtension) + && 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); + } + return null; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java new file mode 100644 index 00000000..0801e9ac --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java @@ -0,0 +1,310 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Application-level singleton managing global scanner state. + * + * Responsibilities: + * - Track which scanners are enabled/disabled globally + * - Sync with user preferences/settings + * - Notify all open projects when scanner state changes + * - Provide query methods for scanner availability + * + * This is an application-scoped service (one instance for entire Eclipse). + * Each project's ScannerRegistry checks this controller before executing scans. + * + * Mirrors the JetBrains GlobalScannerController pattern. + */ +public class GlobalScannerController { + + private static final String LOG_TAG = "[GLOBAL-SCANNER]"; + private static GlobalScannerController instance; + + // Global enable/disable state for each scanner + private final ConcurrentHashMap scannerState = + new ConcurrentHashMap<>(); + + // Listeners notified when scanner state changes + private final List stateListeners = new ArrayList<>(); + + // State manager for persistence + private final com.checkmarx.eclipse.devassist.state.ScannerStateManager stateManager = + new com.checkmarx.eclipse.devassist.state.ScannerStateManager(); + + // Preference change listener to reload state when preferences are saved + private final org.eclipse.jface.util.IPropertyChangeListener prefChangeListener = + event -> { + if ("scannerPreferencesChanged".equals(event.getProperty())) { + reloadStateFromPreferences(); + } + }; + + /** + * 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(); + instance.initializeDefaults(); + instance.registerPreferenceListener(); + } + return instance; + } + + /** + * Initialize scanner state from preferences. + */ + private void initializeDefaults() { + CxLogger.info(LOG_TAG + " Initializing scanner state from preferences"); + + com.checkmarx.eclipse.devassist.state.ScannerState state = stateManager.loadState(); + + for (ScannerType type : ScannerType.values()) { + com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); + if (engine != null) { + boolean enabled = state.isEnabled(engine); + scannerState.put(type, enabled); + String status = enabled ? "enabled" : "disabled"; + CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " " + status); + } + } + } + + /** + * Register listener for preference changes. + * When preferences are saved, reload scanner state from preferences. + */ + private void registerPreferenceListener() { + try { + com.checkmarx.eclipse.Activator.getDefault().getPreferenceStore() + .addPropertyChangeListener(prefChangeListener); + CxLogger.info(LOG_TAG + " Preference listener registered"); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to register preference listener: " + e.getMessage()); + } + } + + /** + * Reload scanner state from preferences. + * Called when preferences are saved to pick up any changes. + */ + private void reloadStateFromPreferences() { + CxLogger.info(LOG_TAG + " Reloading scanner state from preferences"); + + com.checkmarx.eclipse.devassist.state.ScannerState state = stateManager.loadState(); + + for (ScannerType type : ScannerType.values()) { + com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); + if (engine != null) { + boolean enabled = state.isEnabled(engine); + boolean wasEnabled = scannerState.getOrDefault(type, true); + + if (enabled != wasEnabled) { + scannerState.put(type, enabled); + String status = enabled ? "enabled" : "disabled"; + CxLogger.info(LOG_TAG + " Updated " + type.getDisplayName() + " to " + status); + notifyScannerStateChanged(type, enabled); + } + } + } + } + + /** + * 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()); + com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); + if (engine != null) { + stateManager.setScannerEnabled(engine, true); + } + 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()); + com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); + if (engine != null) { + stateManager.setScannerEnabled(engine, false); + } + notifyScannerStateChanged(type, false); + } + } + + /** + * Check if a scanner is enabled globally. + * + * @param type Scanner type to check + * @return true if enabled, false if disabled + */ + public boolean isScannerEnabled(ScannerType type) { + if (type == null) { + return false; + } + + // Default to enabled if not explicitly set + return scannerState.getOrDefault(type, true); + } + + /** + * Enable all scanners. + */ + public void enableAllScanners() { + CxLogger.info(LOG_TAG + " Enabling all scanners"); + + for (ScannerType type : ScannerType.values()) { + enableScanner(type); + } + } + + /** + * Disable all scanners. + */ + public void disableAllScanners() { + CxLogger.info(LOG_TAG + " Disabling all scanners"); + + for (ScannerType type : ScannerType.values()) { + disableScanner(type); + } + } + + /** + * Get count of enabled scanners. + * + * @return Number of enabled scanners + */ + public int getEnabledScannerCount() { + int count = 0; + for (ScannerType type : ScannerType.values()) { + if (isScannerEnabled(type)) { + count++; + } + } + return count; + } + + /** + * Register a listener to be notified of state changes. + * + * @param listener Listener callback + */ + public void addScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.add(listener); + } + } + + /** + * Unregister a state listener. + * + * @param listener Listener to remove + */ + public void removeScannerStateListener(ScannerStateListener listener) { + if (listener != null) { + stateListeners.remove(listener); + } + } + + /** + * Notify all listeners of a scanner state change. + * + * @param type Changed scanner type + * @param enabled New enabled state + */ + private void notifyScannerStateChanged(ScannerType type, boolean enabled) { + for (ScannerStateListener listener : stateListeners) { + try { + listener.onScannerStateChanged(type, enabled); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error notifying listener: " + e.getMessage()); + } + } + } + + /** + * Get a detailed state report. + * + * @return Multi-line status string + */ + public String getStateReport() { + StringBuilder sb = new StringBuilder(); + sb.append(LOG_TAG).append(" Scanner State Report:\n"); + + for (ScannerType type : ScannerType.values()) { + boolean enabled = isScannerEnabled(type); + sb.append(" ").append(type.getDisplayName()).append(": ") + .append(enabled ? "ENABLED" : "DISABLED").append("\n"); + } + + sb.append(" Total Enabled: ").append(getEnabledScannerCount()).append("/") + .append(ScannerType.values().length); + + return sb.toString(); + } + + /** + * Convert ScannerType to ScanEngine enum. + * Used for bridging between global controller and state manager. + * + * @param type Scanner type + * @return Corresponding ScanEngine, or null if no mapping exists + */ + private com.checkmarx.eclipse.devassist.model.ScanEngine typeToEngine(ScannerType type) { + if (type == null) { + return null; + } + + return switch (type) { + case ASCA -> com.checkmarx.eclipse.devassist.model.ScanEngine.ASCA; + case OSS -> com.checkmarx.eclipse.devassist.model.ScanEngine.OSS; + case SECRETS -> com.checkmarx.eclipse.devassist.model.ScanEngine.SECRETS; + case IAC -> com.checkmarx.eclipse.devassist.model.ScanEngine.IAC; + case CONTAINERS -> com.checkmarx.eclipse.devassist.model.ScanEngine.CONTAINERS; + }; + } + + /** + * Listener interface for scanner state changes. + * Implemented by project registries to react to global changes. + */ + public interface ScannerStateListener { + /** + * Called when a scanner's enabled state changes globally. + * + * @param type Changed scanner type + * @param enabled New enabled state + */ + void onScannerStateChanged(ScannerType type, boolean enabled); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java new file mode 100644 index 00000000..eeb46a5d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -0,0 +1,328 @@ +package com.checkmarx.eclipse.devassist.backend; + +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IProject; + +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; + +/** + * Manages the lifecycle of scanner services for a project. + * + * Responsibilities: + * - Create scanner instances when project opens + * - Store scanner instances for reuse + * - Dispose scanners when project closes + * + * This is a project-level service. Each open project gets its own registry. + * Scanners are lazily initialized on first access. + * + * Mirrors the JetBrains ScannerRegistry pattern. + */ +public class ScannerRegistry { + + private static final String LOG_TAG = "[SCANNER-REGISTRY]"; + + // Session property key for storing registry on project + public static final String REGISTRY_KEY = ScannerRegistry.class.getName() + ".INSTANCE"; + + private final IProject project; + private final ConcurrentHashMap scanners = new ConcurrentHashMap<>(); + private boolean disposed = false; + + /** + * Create a registry for a project. + * + * @param project Eclipse project + */ + public ScannerRegistry(IProject project) { + this.project = project; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + /** + * Initialize all available scanners. + * + * Called when project opens. Scanners are created but not yet active; + * activation is controlled by GlobalScannerController. + */ + public void registerAllScanners() { + if (disposed) { + CxLogger.warning(LOG_TAG + " Registry is disposed, cannot register scanners"); + return; + } + + CxLogger.info(LOG_TAG + " Registering all scanners for: " + project.getName()); + + // Scanners will be created lazily via getScannerService() + // For now, just initialize placeholders to track scanner types + ScannerType[] scannerTypes = { + ScannerType.OSS, + ScannerType.SECRETS, + ScannerType.CONTAINERS, + ScannerType.IAC, + ScannerType.ASCA + }; + + for (ScannerType type : scannerTypes) { + CxLogger.info(LOG_TAG + " ✓ Scanner registered: " + type); + } + } + + /** + * Deregister and dispose all scanners (on project close). + */ + public void deregisterAllScanners() { + CxLogger.info(LOG_TAG + " Deregistering all scanners for: " + project.getName()); + + // Dispose each scanner + scanners.forEach((type, scanner) -> { + try { + if (scanner instanceof AutoCloseable) { + ((AutoCloseable) scanner).close(); + } + CxLogger.info(LOG_TAG + " ✓ Disposed scanner: " + type); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing scanner " + type + ": " + + e.getMessage()); + } + }); + + scanners.clear(); + disposed = true; + CxLogger.info(LOG_TAG + " All scanners disposed"); + } + + /** + * Get a scanner service by type. + * Lazily creates the scanner on first access. + * + * @param type Scanner type (OSS, SECRETS, etc.) + * @return Scanner instance, or null if scanner type not supported + */ + public Object getScannerService(ScannerType type) { + if (disposed) { + CxLogger.warning(LOG_TAG + " Registry is disposed"); + return null; + } + + return scanners.computeIfAbsent(type.name(), key -> { + CxLogger.info(LOG_TAG + " Creating scanner: " + type); + // Scanner creation will be implemented in Phase 2 + return createScannerInstance(type); + }); + } + + /** + * Create a scanner instance by type. + * Creates implementations of ScannerService that delegate to the new scanner commands. + * + * @param type Scanner type + * @return Scanner instance + */ + private Object createScannerInstance(ScannerType type) { + try { + CxLogger.info(LOG_TAG + " Creating scanner instance for: " + type.getDisplayName()); + Object scanner = null; + + switch (type) { + case OSS: + scanner = new OssScannerServiceImpl(project); + break; + case SECRETS: + scanner = new SecretsScannerServiceImpl(project); + break; + case CONTAINERS: + scanner = new ContainerScannerServiceImpl(project); + break; + case IAC: + scanner = new IacScannerServiceImpl(project); + break; + case ASCA: + scanner = new AscaScannerServiceImpl(project); + break; + default: + return null; + } + + if (scanner != null) { + CxLogger.info(LOG_TAG + " ✓ Successfully created scanner: " + type.getDisplayName()); + } else { + CxLogger.warning(LOG_TAG + " ⚠ Scanner returned null: " + type.getDisplayName()); + } + return scanner; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " ✗ Error creating scanner " + type.getDisplayName() + ": " + e.getMessage(), e); + e.printStackTrace(); + return null; + } + } + + /** + * Inner class implementations of ScannerService that bridge to new scanner commands. + * These are minimal adapters that delegate to the proper scanner packages. + */ + + private static class OssScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand command; + OssScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand(project); + } + @Override + public boolean shouldScanFile(String filePath) { return command.shouldScan(filePath); } + @Override + public java.util.List scan(String filePath) throws Exception { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return result != null ? result.getIssues() : java.util.List.of(); + } + @Override + public String getDisplayName() { return "Open Source Supply Chain"; } + @Override + public ScannerType getScannerType() { return ScannerType.OSS; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class SecretsScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand command; + SecretsScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand(project); + } + @Override + public boolean shouldScanFile(String filePath) { return command.shouldScan(filePath); } + @Override + public java.util.List scan(String filePath) throws Exception { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return result != null ? result.getIssues() : java.util.List.of(); + } + @Override + public String getDisplayName() { return "Secrets Scanning"; } + @Override + public ScannerType getScannerType() { return ScannerType.SECRETS; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class IacScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand command; + IacScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand(project); + } + @Override + public boolean shouldScanFile(String filePath) { return command.shouldScan(filePath); } + @Override + public java.util.List scan(String filePath) throws Exception { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return result != null ? result.getIssues() : java.util.List.of(); + } + @Override + public String getDisplayName() { return "Infrastructure as Code"; } + @Override + public ScannerType getScannerType() { return ScannerType.IAC; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class AscaScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand command; + AscaScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand(project); + } + @Override + public boolean shouldScanFile(String filePath) { return command.shouldScan(filePath); } + @Override + public java.util.List scan(String filePath) throws Exception { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return result != null ? result.getIssues() : java.util.List.of(); + } + @Override + public String getDisplayName() { return "Application Security Code Analysis"; } + @Override + public ScannerType getScannerType() { return ScannerType.ASCA; } + @Override + public void close() throws Exception { command.dispose(); } + } + + private static class ContainerScannerServiceImpl implements ScannerService { + private final com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand command; + ContainerScannerServiceImpl(IProject project) { + this.command = new com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand(project); + } + @Override + public boolean shouldScanFile(String filePath) { return command.shouldScan(filePath); } + @Override + public java.util.List scan(String filePath) throws Exception { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return result != null ? result.getIssues() : java.util.List.of(); + } + @Override + public String getDisplayName() { return "Container Scanning"; } + @Override + public ScannerType getScannerType() { return ScannerType.CONTAINERS; } + @Override + public void close() throws Exception { command.dispose(); } + } + + /** + * Check if a scanner is registered. + * + * @param type Scanner type + * @return true if scanner exists + */ + public boolean hasScannerService(ScannerType type) { + return scanners.containsKey(type.name()); + } + + /** + * Get the project this registry belongs to. + * + * @return Eclipse project + */ + public IProject getProject() { + return project; + } + + /** + * Check if registry is disposed. + * + * @return true if disposed + */ + public boolean isDisposed() { + return disposed; + } + + /** + * Get statistics for debugging. + * + * @return Summary string + */ + public String getStatistics() { + return "Project: " + project.getName() + + ", Scanners: " + scanners.size() + + ", Disposed: " + disposed; + } + + /** + * Enum of available scanner types. + * Maps to the 5 scanner engines in Checkmarx. + */ + public enum ScannerType { + OSS("Open Source Supply Chain"), + SECRETS("Secrets Scanning"), + CONTAINERS("Container Scanning"), + IAC("Infrastructure as Code"), + ASCA("Application Security Code Analysis"); + + private final String displayName; + + ScannerType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java new file mode 100644 index 00000000..459da6f8 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/SeverityLevel.java @@ -0,0 +1,51 @@ +package com.checkmarx.eclipse.devassist.backend; + +/** + * Severity level enumeration matching JetBrains implementation. + * Provides 8 severity levels with precedence values (lower = more severe). + */ +public enum SeverityLevel { + MALICIOUS("Malicious", 1), + CRITICAL("Critical", 2), + HIGH("High", 3), + MEDIUM("Medium", 4), + LOW("Low", 5), + UNKNOWN("Unknown", 6), + OK("OK", 7), + IGNORED("Ignored", 8); + + private final String severity; + private final int precedence; + + SeverityLevel(String severity, int precedence) { + this.severity = severity; + this.precedence = precedence; + } + + public String getSeverity() { + return severity; + } + + public int getPrecedence() { + return precedence; + } + + /** + * Convert string severity value to enum. + * Returns UNKNOWN if no match found. + * + * @param value Severity string (case-insensitive) + * @return Matching SeverityLevel or UNKNOWN + */ + public static SeverityLevel fromValue(String value) { + if (value == null) { + return UNKNOWN; + } + for (SeverityLevel level : values()) { + if (level.getSeverity().equalsIgnoreCase(value)) { + return level; + } + } + return UNKNOWN; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java new file mode 100644 index 00000000..0184196d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -0,0 +1,313 @@ +package com.checkmarx.eclipse.devassist.backend.listener; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.result.ResultPublisher; +import com.checkmarx.eclipse.devassist.basescanner.ScanManager; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; + +public class ProjectLifecycleListener implements IResourceChangeListener { + + private static final String LOG_TAG = "[PROJECT-LISTENER]"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + + private static final QualifiedName REGISTRY_KEY = new QualifiedName(PLUGIN_ID, "scanner-registry"); + private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); + private static final QualifiedName STATE_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "state-holder"); + + private final List initializedProjects = new ArrayList<>(); + + /** + * Register this listener with Eclipse workspace and process existing open projects. + */ + public void register() { + CxLogger.info(LOG_TAG + " Registering project lifecycle listener"); + ResourcesPlugin.getWorkspace().addResourceChangeListener( + this, + IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE + ); + CxLogger.info(LOG_TAG + " ✓ Registered"); + + // FIX 1: Run immediate initialization for projects ALREADY open on IDE startup + initExistingProjects(); + } + + /** + * Re-runs initialization (registry setup + initial OSS/IaC/container scan) for + * any already-open projects that were skipped earlier because the user wasn't + * authenticated yet - the exact same path {@link #register()} runs for + * already-open projects at plugin launch. onProjectOpen() only proceeds when + * isUserAuthenticated() is true and nothing else ever re-triggers it for a + * project that was already open (only a real open/close event does), so a + * login that happens after Eclipse already started needs to call this to get + * the same initial scan that a fresh launch would have performed. + */ + public void scanAlreadyOpenProjects() { + initExistingProjects(); + } + + /** + * Scans the workspace and initializes any projects that are already open. + */ + private void initExistingProjects() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (IProject project : projects) { + if (project.isOpen() && !isInitialized(project)) { + System.out.println(LOG_TAG + " Found existing open project on startup: " + project.getName()); + onProjectOpen(project); + } + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error initializing existing projects on startup: " + e.getMessage(), e); + } + } + + public void unregister() { + CxLogger.info(LOG_TAG + " Unregistering project lifecycle listener"); + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + } + + /** + * Handle resource change events for project state changes (open/close). + */ + @Override + public void resourceChanged(IResourceChangeEvent event) { + try { + // Handle project close (PRE_CLOSE) + if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + IResource resource = event.getResource(); + if (resource instanceof IProject) { + onProjectClose((IProject) resource); + } + return; + } + + // FIX 2: Inspect IResourceDelta to catch when a closed project is opened manually + if (event.getType() == IResourceChangeEvent.POST_CHANGE && event.getDelta() != null) { + event.getDelta().accept(delta -> { + IResource resource = delta.getResource(); + if (resource instanceof IProject) { + IProject project = (IProject) resource; + // Check if project OPEN state changed + if ((delta.getFlags() & IResourceDelta.OPEN) != 0) { + if (project.isOpen() && !isInitialized(project)) { + onProjectOpen(project); + } else if (!project.isOpen() && isInitialized(project)) { + onProjectClose(project); + } + } + } + // Only visit top-level delta children (projects are at root level) + return true; + }); + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error handling resource change: " + e.getMessage(), e); + } + } + + private void onProjectOpen(IProject project) { + String projName = project.getName(); + if (projName.length() > 26) projName = projName.substring(0, 26); + try { + if (!isUserAuthenticated()) { + return; + } + + ScannerRegistry registry = new ScannerRegistry(project); + registry.registerAllScanners(); + project.setSessionProperty(REGISTRY_KEY, registry); + + ProblemHolderService problemHolder = new ProblemHolderService(); + project.setSessionProperty(PROBLEM_HOLDER_KEY, problemHolder); + DevAssistScanStateHolder stateHolder = new DevAssistScanStateHolder(); + project.setSessionProperty(STATE_HOLDER_KEY, stateHolder); + initializedProjects.add(project.getName()); + + startWorkspaceFileScanning(project); + + } catch (Exception e) { + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error initializing project " + + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isUserAuthenticated() { + String apiKey = com.checkmarx.eclipse.properties.Preferences.getApiKey(); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + private void onProjectClose(IProject project) { + CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); + + try { + try { + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); + if (registry != null) { + registry.deregisterAllScanners(); + CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing ScannerRegistry: " + e.getMessage()); + } + + try { + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + if (problemHolder != null) { + problemHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing cache: " + e.getMessage()); + } + + try { + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + if (stateHolder != null) { + stateHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ State holder cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing state: " + e.getMessage()); + } + + initializedProjects.remove(project.getName()); + CxLogger.info(LOG_TAG + " ✓ Project cleanup completed: " + project.getName()); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error cleaning up project " + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isInitialized(IProject project) { + return initializedProjects.contains(project.getName()); + } + + public String getStatistics() { + return "Initialized projects: " + initializedProjects.size(); + } + + private void startWorkspaceFileScanning(IProject project) { + Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Scanning manifest, IaC, and container files...", 3); + + scanManifestFiles(project); + monitor.worked(1); + + scanIacFiles(project); + monitor.worked(1); + + scanContainerFiles(project); + monitor.worked(1); + + return Status.OK_STATUS; + + } catch (Exception e) { + e.printStackTrace(); + return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); + } finally { + monitor.done(); + } + } + }; + + // Run as a background job so it doesn't block the IDE + scanJob.setPriority(Job.BUILD); + scanJob.schedule(); + } + + private void scanManifestFiles(IProject project) { + String[] manifestPatterns = { + "pom.xml", "package.json", "package-lock.json", "npm-shrinkwrap.json", + "go.mod", "go.sum", "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", + "Gemfile", "Gemfile.lock", "Cargo.toml", "Cargo.lock", "composer.json", "composer.lock", + "packages.config", ".csproj", "yarn.lock" + }; + findAndScanFiles(project, manifestPatterns, "OSS Manifest Files"); + } + + private void scanIacFiles(IProject project) { + String[] iacPatterns = { ".tf", ".tfvars", ".yaml", ".yml", ".hcl" }; + findAndScanFiles(project, iacPatterns, "IaC Configuration Files"); + } + + private void scanContainerFiles(IProject project) { + String[] containerPatterns = { + "Dockerfile", "dockerfile", "docker-compose.yaml", "docker-compose.yml", ".dockerignore" + }; + findAndScanFiles(project, containerPatterns, "Container Files"); + } + + private void findAndScanFiles(IProject project, String[] patterns, String fileType) { + try { + System.out.println(LOG_TAG + " â–º Scanning for " + fileType + "..."); + + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "scanner-registry")); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "state-holder")); + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "problem-holder")); + + if (registry == null || stateHolder == null || problemHolder == null) { + return; + } + + IResource[] members = project.members(true); + for (IResource resource : members) { + if (!(resource instanceof org.eclipse.core.resources.IFile)) { + continue; + } + + IFile file = (org.eclipse.core.resources.IFile) resource; + String fileName = file.getName().toLowerCase(); + String filePath = file.getLocation().toOSString(); + + boolean matches = false; + for (String pattern : patterns) { + if (fileName.equals(pattern.toLowerCase()) || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { + matches = true; + break; + } + } + if (matches) { + try { + ScanManager scanManager = new ScanManager(registry, stateHolder); + List issues = scanManager.scanFile(filePath); + if (!issues.isEmpty()) { + problemHolder.addScanIssues(filePath, issues); + ResultPublisher.publishResults(file, issues); + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error scanning " + fileName + ": " + e.getMessage()); + } + } + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error finding files for " + fileType + ": " + e.getMessage()); + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java new file mode 100644 index 00000000..797351f2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -0,0 +1,256 @@ +package com.checkmarx.eclipse.devassist.backend.result; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.inspection.DevAssistInspectionMgr; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.List; + +/** + * Publishes scan results to Checkmarx Findings Window and editor decorations. + * + * Responsibilities: + * - Update custom Findings View with scan results + * - Render editor decorations (gutter icons, underlines) for Findings Window issues + * - NO integration with Eclipse native Problems View + * + * This connects scan results directly to the custom Findings Window. + */ +public class ResultPublisher { + + private static final String LOG_TAG = "[RESULT-PUBLISHER]"; + + /** + * Publish scan results to Findings View and editor decorations. + * + * Orchestrates the complete problem descriptor creation and publication flow: + * 1. Update Findings View cache with scan results + * 2. Create problem descriptors via DevAssistInspectionMgr + * 3. Render editor decorations (gutter icons, underlines) + * + * Mirrors JetBrains pattern where scan results are stored in cache, + * which then publishes a message to notify all interested views. + * + * @param file File that was scanned + * @param scanIssues Issues found by scanners + */ + public static void publishResults(IFile file, List scanIssues) { + if (file == null || scanIssues == null) { + return; + } + try { + // Step 1: Update Findings View (try to display immediately if view is open) + System.out.println(LOG_TAG + " [STEP 1/3] Attempting to update Findings View if open..."); + updateFindingsView(file, scanIssues); + System.out.println(LOG_TAG + " [OK] Findings View update attempted"); + + // Step 2: Create problem descriptors via DevAssistInspectionMgr + System.out.println(LOG_TAG + " [STEP 2/3] Creating problem descriptors..."); + createAndRenderDecorations(file, scanIssues); + System.out.println(LOG_TAG + " [OK] Problem descriptors created and rendered"); + + } catch (Exception e) { + System.err.println(LOG_TAG + " [ERROR] " + e.getMessage()); + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error publishing results: " + e.getMessage(), e); + } + } + + /** + * Update Findings View with scan results. + * + * @param file File that was scanned + * @param scanIssues Issues to display + */ + private static void updateFindingsView(IFile file, List scanIssues) { + try { + if (scanIssues.isEmpty()) { + return; + } + + // Must run on UI thread + org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + + // JetBrains Pattern: Remove old engine results, then merge new results + // This triggers the message bus pattern: + // 1. removeScanIssuesByFileAndScanner() removes old results for THIS engine + // 2. mergeScanIssues() stores new results in cache + // 3. notifyListenersOfUpdate() publishes to all listeners + // 4. CxFindingsView listener receives callback with getAllIssues() + // 5. Listener calls refreshTree(allCachedResults) + // 6. Tree shows merged results (no duplicates, no stale issues) + // FIX: Use getLocation() (absolute path) to match cache key format used in RealTimeScanJob + // ProblemHolderService cache is keyed with absolute paths from RealTimeScanJob.scanFile() + // Must use same path format for cache lookups or removal will fail - causing duplicates + String filePath = file.getLocation().toOSString(); + + org.eclipse.core.resources.IProject project = file.getProject(); + if (project != null) { + ProblemHolderService problemHolder = + (ProblemHolderService) project.getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + // Get engine type from scan issues (all issues from same scan have same engine) + String engineType = scanIssues.isEmpty() ? null : + scanIssues.get(0).getScanEngine() != null ? + scanIssues.get(0).getScanEngine().name() : null; + + // Step 1: Remove old results from THIS scanner engine + if (engineType != null) { + problemHolder.removeScanIssuesByFileAndScanner(engineType, filePath); + System.out.println(LOG_TAG + " [REMOVE] Removed old " + engineType + " issues for: " + filePath); + } + + // Step 2: Add new results from THIS scanner engine + problemHolder.mergeScanIssues(filePath, scanIssues); + System.out.println(LOG_TAG + " [MERGE] Merged " + scanIssues.size() + " new issues from " + + (engineType != null ? engineType : "UNKNOWN") + " for: " + filePath); + } else { + System.out.println(LOG_TAG + " [VIEW-UPDATE] ProblemHolderService not initialized - results not cached"); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Create problem descriptors and render editor decorations. + * + * Orchestrates: + * 1. Get registry and state holder from project session + * 2. Build ProblemHelper.Builder with file context and scan issues + * 3. Call DevAssistInspectionMgr to create problem descriptors + * 4. Render gutter icons and underlines using descriptors + * + * @param file File that was scanned + * @param scanIssues Issues to process + */ + private static void createAndRenderDecorations(IFile file, List scanIssues) { + try { + if (scanIssues.isEmpty()) { + return; + } + + org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + + org.eclipse.core.resources.IProject project = file.getProject(); + if (project == null) { + CxLogger.warning(LOG_TAG + " Project not available for file: " + file.getName()); + return; + } + + display.asyncExec(() -> { + try { + // Get registry and state holder from session properties + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "scanner-registry")); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "scan-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(); + ProblemHelper.Builder builder = ProblemHelper.builder(file, project) + .filePath(filePath) + .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()); + } + } + + /** + * Find the open Findings View. + * + * @return CxFindingsView instance if open, null otherwise + */ + private static com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView findOpenFindingsView() { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + + IWorkbenchPage page = null; + try { + page = workbench.getActiveWorkbenchWindow().getActivePage(); + } catch (NullPointerException e) { + for (var window : workbench.getWorkbenchWindows()) { + page = window.getActivePage(); + if (page != null) break; + } + } + + if (page == null) { + return null; + } + + return (com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView) page + .findView(com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView.ID); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error finding Findings View: " + e.getMessage()); + return null; + } + } + + /** + * Clear results for a file. + * + * @param file File to clear + */ + public static void clearResults(IFile file) { + try { + CxLogger.info(LOG_TAG + " Clearing results for: " + file.getName()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing results: " + e.getMessage()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java new file mode 100644 index 00000000..c7ba8d8b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java @@ -0,0 +1,150 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; +import java.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 ScannerService providing common functionality. + * + * Provides: + * - File filtering (node_modules exclusion, etc.) + * - Temporary folder management + * - Template methods for subclasses + */ +public abstract class BaseScannerService implements ScannerService { + + protected final IProject project; + protected final String logTag; + + /** + * Create a scanner for a project. + * + * @param project Eclipse project + */ + public BaseScannerService(IProject project) { + this.project = project; + this.logTag = "[" + getScannerName() + "-SCANNER]"; + } + + /** + * Check if file should be scanned. + * + * Applies common exclusions then delegates to subclass for type checking. + * + * @param filePath File path + * @param project Eclipse project + * @return true if file should be scanned + */ + @Override + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + + // Common exclusions + if (isCommonlyExcluded(filePath)) { + return false; + } + + return isFileTypeSupported(filePath); + } + + /** + * Apply common exclusions. + * + * @param filePath File path + * @return true if file should be excluded + */ + private boolean isCommonlyExcluded(String filePath) { + return filePath.contains("/node_modules/") || filePath.contains("\\node_modules\\"); + } + + /** + * Subclasses implement scanner-specific file type checking. + * + * @param filePath File path + * @return true if scanner supports this file + */ + protected abstract boolean isFileTypeSupported(String filePath); + + /** + * Get the scanner name for logging (e.g., "OSS", "SECRETS"). + * + * @return Scanner name + */ + protected abstract String getScannerName(); + + /** + * Get the log tag for this scanner. + * + * @return Log tag + */ + protected String getLogTag() { + return logTag; + } + + /** + * Build path to temp sub-folder in system temp directory. + * + * @param baseDir Sub-folder name + * @return Absolute path to temp directory + */ + protected String getTempSubFolderPath(String baseDir) { + String tempOS = System.getProperty("java.io.tmpdir"); + Path tempDir = Paths.get(tempOS, baseDir); + return tempDir.toString(); + } + + /** + * Create temp folder if it doesn't exist. + * + * @param folderPath Folder path + */ + protected void createTempFolder(Path folderPath) { + try { + Files.createDirectories(folderPath); + } catch (IOException e) { + CxLogger.warning("Failed to create temp folder: " + folderPath); + } + } + + /** + * Recursively delete temp folder and contents. + * + * @param tempFolder Folder to delete + */ + 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 temp file: " + path); + } + }); + } catch (IOException e) { + CxLogger.warning("Failed to delete temp folder: " + tempFolder); + } + } + + /** + * Close the scanner and release resources. + * + * @throws Exception if close fails + */ + public void close() throws Exception { + CxLogger.info(logTag + " Closed for project: " + project.getName()); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java new file mode 100644 index 00000000..8f06812c --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java @@ -0,0 +1,182 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.common.ScannerFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Orchestrates the scanning process for a file. + * + * Responsibilities: - Use ScannerFactory to select appropriate scanners for a + * file - Check if file has changed since last scan (skip redundant scans) - + * Execute all applicable scanners in sequence - Merge results from multiple + * scanners - Update file state timestamp to prevent re-scanning + * + * This is the main entry point for initiating scans. Called from + * FileEditorListener when a file is modified. + * + * NOTE: Uses backend.DevAssistScanStateHolder (not inspection.version) to maintain compatibility + * with existing code that passes backend version to super(). + */ +public class ScanManager { + + private static final String LOG_TAG = "[SCAN-MANAGER]"; + + private final ScannerFactory factory; + private final com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder; + + /** + * Create a scan manager for a project. + * + * @param registry Project's scanner registry + * @param stateHolder State holder for tracking file modification + */ + public ScanManager(ScannerRegistry registry, DevAssistScanStateHolder stateHolder) { + this.factory = new ScannerFactory(registry); + this.stateHolder = stateHolder; + } + + /** + * Scan a file using all applicable scanners. + * + * High-level flow: 1. Compute current file state hash 2. Check if file changed + * since last scan 3. If unchanged, return cached results 4. Get all scanners + * that support this file 5. Execute each scanner sequentially 6. Merge results + * from all scanners 7. Update state hash to mark as scanned 8. Return merged + * results + * + * @param filePath Absolute file path to scan + * @return List of issues found by all scanners + * @throws Exception if scan fails + */ + public List scanFile(String filePath) throws Exception { + if (filePath == null || filePath.isEmpty()) { + System.out.println(LOG_TAG + " ✗ BLOCKED: Null or empty file path"); + return List.of(); + } + + System.out.println(LOG_TAG + " ╔════════════════════════════════════════════╗"); + System.out.println(LOG_TAG + " â•‘ SCAN MANAGER: Starting file scan â•‘"); + System.out.println(LOG_TAG + " ╚════════════════════════════════════════════╝"); + System.out.println(LOG_TAG + " File path: " + filePath); + + // 1. Compute current file state hash + System.out.println(LOG_TAG + " [STEP 1/5] Computing file state hash..."); + long currentStateHash = DevAssistScanStateHolder.computeFileStateHash(filePath); + System.out.println(LOG_TAG + " ✓ File hash: " + currentStateHash); + + // 2. Check if file changed since last scan + System.out.println(LOG_TAG + " [STEP 2/5] Checking if file changed..."); + if (!stateHolder.hasChanged(filePath, currentStateHash)) { + System.out.println(LOG_TAG + " ℹ️ File unchanged since last scan - skipping (cache result)"); + return List.of(); + } + System.out.println(LOG_TAG + " ✓ File changed - proceeding with scan"); + + // 3. Get all scanners that support this file + System.out.println(LOG_TAG + " [STEP 3/5] Getting applicable scanners..."); + List applicableScanners = factory.getAllSupportedScanners(filePath); + + System.out.println(LOG_TAG + " ✓ Found " + applicableScanners.size() + " applicable scanners:"); + for (ScannerService scanner : applicableScanners) { + System.out.println(LOG_TAG + " - " + scanner.getDisplayName()); + } + + if (applicableScanners.isEmpty()) { + System.out.println(LOG_TAG + " ℹ️ No scanners support this file type - skipping"); + // Still update state to avoid re-checking unsupported files + stateHolder.updateStateHash(filePath, currentStateHash); + return List.of(); + } + + // 4. Execute all scanners and merge results + System.out.println(LOG_TAG + " [STEP 4/5] Executing scanners..."); + List allIssues = new ArrayList<>(); + int scannerIndex = 1; + + for (ScannerService scanner : applicableScanners) { + try { + System.out.println(LOG_TAG + " [" + scannerIndex + "/" + applicableScanners.size() + "] Executing " + + scanner.getDisplayName() + "..."); + + List scannerResults = scanner.scan(filePath); + + if (scannerResults == null) { + System.out.println( + LOG_TAG + " ⚠️ WARNING: " + scanner.getDisplayName() + " returned NULL results!"); + } else { + System.out.println(LOG_TAG + " ✓ " + scanner.getDisplayName() + " returned " + + scannerResults.size() + " issues"); + for (ScanIssue issue : scannerResults) { + System.out.println( + LOG_TAG + " - " + issue.getTitle() + " (severity: " + issue.getSeverity() + ")"); + } + allIssues.addAll(scannerResults); + } + + } catch (Exception e) { + // Log but continue with other scanners + System.err.println(LOG_TAG + " ✗ ERROR in " + scanner.getDisplayName() + ": " + e.getMessage()); + e.printStackTrace(); + } + scannerIndex++; + } + + // 5. Update state hash to mark as scanned + stateHolder.updateStateHash(filePath, currentStateHash); + + return allIssues; + } + + /** + * 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 + " " + scannerType.getDisplayName() + " does not support file: " + filePath); + return List.of(); + } + + try { + List results = scanner.scan(filePath); + CxLogger.info(LOG_TAG + " ✓ " + scannerType.getDisplayName() + " found " + results.size() + " issues"); + return results; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " " + scannerType.getDisplayName() + " scan failed: " + e.getMessage(), e); + throw e; + } + } + + /** + * Get factory statistics. + * + * @return Summary string + */ + public String getStatistics() { + return factory.getStatistics(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java new file mode 100644 index 00000000..544df8f0 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java @@ -0,0 +1,73 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import java.util.List; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Interface for all scanner implementations. + * + * Each scanner (OSS, Secrets, ASCA, Containers, IAC) implements this + * to provide consistent scan execution and file type detection. + */ +public interface ScannerService extends AutoCloseable { + + /** + * Check if this scanner supports a file type. + * + * @param filePath File path to check + * @return true if this scanner can scan this file + */ + boolean shouldScanFile(String filePath); + + /** + * Execute a scan on a file. + * + * @param filePath Absolute file path to scan + * @return List of issues found by this scanner + * @throws Exception if scan fails + */ + List scan(String filePath) throws Exception; + + /** + * Get the display name of this scanner. + * + * @return Human-readable name (e.g., "Open Source Supply Chain") + */ + String getDisplayName(); + + /** + * Get the scanner type. + * + * @return Scanner type enum + */ + ScannerType getScannerType(); + + /** + * Cleanup resources when scanner is no longer needed. + */ + @Override + void close() throws Exception; + + /** + * Scanner type enumeration. + */ + enum ScannerType { + OSS("Open Source Supply Chain"), + SECRETS("Secrets Scanning"), + CONTAINERS("Container Scanning"), + IAC("Infrastructure as Code"), + ASCA("Application Security Code Analysis"); + + private final String displayName; + + ScannerType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java new file mode 100644 index 00000000..1b7f240f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanResult.java @@ -0,0 +1,32 @@ +package com.checkmarx.eclipse.devassist.common; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import java.util.List; + +/** + * Interface for a scan result wrapper. + * + * Adaptor classes implement this interface to wrap raw scanner results + * and provide conversion to standardized ScanIssue objects. + * + * @param Type of raw scanner result (e.g., OssRealtimeResults, SecretsRealtimeResults) + */ +public interface ScanResult { + + /** + * Get the raw scan results from the scanner. + * + * @return Raw scanner results of type T + */ + T getResults(); + + /** + * Get the standardized list of scan issues from the raw results. + * + * This converts the scanner-specific result format into a uniform + * list of ScanIssue objects that can be displayed in the UI. + * + * @return List of ScanIssue objects + */ + List getIssues(); +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java new file mode 100644 index 00000000..3e39f9fd --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java @@ -0,0 +1,189 @@ +package com.checkmarx.eclipse.devassist.common; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; + +/** + * Factory for selecting appropriate scanners by file type. + * + * Responsibilities: + * - Query all available scanners + * - Filter by file type compatibility + * - Filter by global enabled state + * - Return ordered list of applicable scanners + * + * Mirrors the JetBrains ScannerFactory pattern. + */ +public class ScannerFactory { + + private static final String LOG_TAG = "[SCANNER-FACTORY]"; + + private final ScannerRegistry registry; + private final GlobalScannerController controller; + + /** + * Create a scanner factory for a project. + * + * @param registry Project's scanner registry + */ + public ScannerFactory(ScannerRegistry registry) { + this.registry = registry; + this.controller = GlobalScannerController.getInstance(); + } + + /** + * Get all enabled scanners that support a file. + * + * Queries all scanner types, filters by: + * 1. Global enabled state (GlobalScannerController) + * 2. File type support (ScannerService.shouldScanFile()) + * + * @param filePath File to scan + * @return List of applicable scanners (empty if none match) + */ + public List getAllSupportedScanners(String filePath) { + List supported = new ArrayList<>(); + + CxLogger.info(LOG_TAG + " Finding scanners for: " + filePath); + + // Check each scanner type + for (ScannerType type : ScannerType.values()) { + // Check if globally enabled + if (!controller.isScannerEnabled(type)) { + CxLogger.info(LOG_TAG + " ⊘ " + type.getDisplayName() + " disabled globally"); + continue; + } + + // Get scanner from registry + ScannerService scanner = getScannerService(type); + if (scanner == null) { + CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); + continue; + } + + // Check if supports this file type + if (scanner.shouldScanFile(filePath)) { + supported.add(scanner); + CxLogger.info(LOG_TAG + " ✓ " + type.getDisplayName() + " supports file"); + } else { + CxLogger.info(LOG_TAG + " ⊘ " + type.getDisplayName() + " does not support file"); + } + } + + if (supported.isEmpty()) { + CxLogger.info(LOG_TAG + " No scanners support this file"); + } else { + CxLogger.info(LOG_TAG + " ✓ Found " + supported.size() + " supporting scanner(s)"); + } + + return supported; + } + + /** + * Get a specific scanner by type if it supports the file. + * + * @param filePath File to scan + * @param type Scanner type to retrieve + * @return Scanner if enabled and supports file, null otherwise + */ + public ScannerService getScannerForFile(String filePath, ScannerType type) { + if (filePath == null || type == null) { + return null; + } + + // Check if globally enabled + if (!controller.isScannerEnabled(type)) { + CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " is disabled globally"); + return null; + } + + // Get scanner from registry + ScannerService scanner = getScannerService(type); + if (scanner == null) { + CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); + return null; + } + + // Check if supports file type + if (!scanner.shouldScanFile(filePath)) { + CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " does not support file: " + + filePath); + return null; + } + + return scanner; + } + + /** + * Get a scanner service by type. + * Retrieves from the registry which manages scanner lifecycle. + * + * @param type Scanner type + * @return Scanner instance, or null if not available + */ + private ScannerService getScannerService(ScannerType type) { + try { + Object scanner = registry.getScannerService(type); + return scanner instanceof ScannerService ? (ScannerService) scanner : null; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error getting scanner for type " + type + ": " + e.getMessage()); + return null; + } + } + + /** + * Get scanner by file name pattern (useful for quick lookups). + * Returns the primary scanner for a file type. + * + * @param filePath File path + * @return Primary scanner type for this file, or null + */ + public ScannerType getPrimaryScannerType(String filePath) { + if (filePath == null) { + return null; + } + + String lowerPath = filePath.toLowerCase(); + + // Manifest files → OSS + if (lowerPath.matches(".*\\.(package\\.json|pom\\.xml|go\\.mod|requirements\\.txt|" + + "Gemfile|Cargo\\.toml|Pipfile)$")) { + return ScannerType.OSS; + } + + // Source code files → ASCA + if (lowerPath.matches(".*\\.(java|py|js|ts|cpp|cs|go|php|rb|swift)$")) { + return ScannerType.ASCA; + } + + // Infrastructure files → IAC + if (lowerPath.matches(".*\\.(tf|yaml|yml|json|hcl)$")) { + return ScannerType.IAC; + } + + // Container files → CONTAINERS + if (lowerPath.matches(".*(Dockerfile|docker-compose\\.ya?ml)")) { + return ScannerType.CONTAINERS; + } + + // Everything else can be scanned for secrets + return ScannerType.SECRETS; + } + + /** + * Get factory statistics. + * + * @return Summary string + */ + public String getStatistics() { + int enabledCount = controller.getEnabledScannerCount(); + return "Scanners enabled: " + enabledCount + "/" + ScannerType.values().length; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java new file mode 100644 index 00000000..d5c1f8f2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java @@ -0,0 +1,49 @@ +package com.checkmarx.eclipse.devassist.factory; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.properties.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Builds wrapper objects according to the current configuration. + */ +public class CxWrapperFactory { + + public static CxWrapper build() throws CxException, Exception { + return getWrapper(); + } + + /** + * Create a CxWrapper with current credentials and configuration + * + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + private static CxWrapper getWrapper() throws Exception { + CxWrapper cxWrapper = null; + + Logger log = LoggerFactory.getLogger(CxWrapperFactory.class.getName()); + + CxConfig.CxConfigBuilder builder = CxConfig.builder() + .apiKey(Preferences.getApiKey()) + .additionalParameters(Preferences.getAdditionalOptions()); + + CxConfig config = builder.build(); + + try { + cxWrapper = new CxWrapper(config, log); + } catch (IOException e) { + CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); + throw new Exception(e); + } + + return cxWrapper; + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java new file mode 100644 index 00000000..8dbd41d1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspection.java @@ -0,0 +1,45 @@ +package com.checkmarx.eclipse.devassist.inspection; + +/** + * Inspection metadata and registry class. + * + * In JetBrains: extends LocalInspectionTool with checkFile() implementation. + * In Eclipse: serves as metadata holder for inspection framework integration. + * + * Provides inspection ID, name, and grouping constants for registration. + * Can be extended with inspection framework hooks in future. + */ +public class DevAssistInspection { + + // Inspection identity constants + private static final String INSPECTION_ID = "com.checkmarx.eclipse.devassist.inspection"; + private static final String INSPECTION_NAME = "Checkmarx Developer Assist"; + private static final String INSPECTION_GROUP = "Checkmarx"; + + /** + * Get the unique identifier for this inspection. + * + * @return Inspection ID for registration and lookup + */ + public String getInspectionId() { + return INSPECTION_ID; + } + + /** + * Get the human-readable name for this inspection. + * + * @return Inspection name for display in UI + */ + public String getInspectionName() { + return INSPECTION_NAME; + } + + /** + * Get the inspection group/category. + * + * @return Inspection group for organization in preferences + */ + public String getInspectionGroup() { + return INSPECTION_GROUP; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java new file mode 100644 index 00000000..2690357c --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java @@ -0,0 +1,334 @@ +package com.checkmarx.eclipse.devassist.inspection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.basescanner.ScanManager; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemBuilder; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; +import com.checkmarx.eclipse.devassist.problems.ProblemDescriptor; +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.problems.ScanIssueProcessor; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Main orchestrator for inspection workflow. + * + * Coordinates the complete flow: + * 1. Scan files using ScanManager (inherited) + * 2. Create problem descriptors from scan issues + * 3. Validate issues (ScanIssueProcessor) + * 4. Cache problems (ProblemHolderService) + * 5. Decorate editor (ProblemDecorator) + * 6. Manage cleanup and state reset + * + * Extends ScanManager to inherit scanning capabilities. + * Mirrors JetBrains DevAssistInspectionMgr. + */ +public class DevAssistInspectionMgr extends ScanManager { + + private static final String LOG_TAG = "[INSPECTION-MGR]"; + + private final ProblemDecorator problemDecorator = new ProblemDecorator(); + + /** + * Constructor accepting scanner registry and state holder. + * + * @param registry Scanner registry for the project + * @param stateHolder State holder for tracking file modifications + */ + public DevAssistInspectionMgr( + ScannerRegistry registry, + com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder) { + super(registry, stateHolder); + } + + /** + * Scan a file and create problem descriptors. + * + * Complete orchestration: + * 1. Build problem helper + * 2. Scan file → get ScanIssue list (if not already provided) + * 3. Cache scan issues + * 4. Create ScanIssueProcessor for validation + * 5. For each issue: validate and create ProblemDescriptor + * 6. Cache problem descriptors + * 7. Return array of problem descriptors + * + * @param problemHelperBuilder Builder with pre-configured context + * @return Array of problem descriptors (empty if none) + */ + public ProblemDescriptor[] startScanAndCreateProblemDescriptors( + ProblemHelper.Builder problemHelperBuilder) { + + ProblemHelper problemHelper = problemHelperBuilder.build(); + + CxLogger.info(LOG_TAG + " Starting scan for file: " + problemHelper.getFile().getName()); + + try { + // Use pre-scanned issues if available, otherwise scan file + List allScanIssues = problemHelper.getScanIssueList(); + if (allScanIssues == null || allScanIssues.isEmpty()) { + allScanIssues = scanFile(problemHelper.getFilePath()); + CxLogger.info(LOG_TAG + " Performed fresh scan for file: " + problemHelper.getFile().getName()); + } else { + CxLogger.info(LOG_TAG + " Using pre-scanned issues for file: " + problemHelper.getFile().getName()); + } + + if (allScanIssues.isEmpty()) { + CxLogger.info(LOG_TAG + " No scan issues found for: " + + problemHelper.getFile().getName()); + decorateUIForIgnoreVulnerability(problemHelper.getFile(), allScanIssues); + return new ProblemDescriptor[0]; + } + + // Ensure helper has the issues (in case they were pre-populated) + problemHelperBuilder.scanIssueList(allScanIssues); + ProblemHelper helperWithIssues = problemHelperBuilder.build(); + + // Cache issues + helperWithIssues.getProblemHolderService().addScanIssues( + problemHelper.getFilePath(), allScanIssues); + + // Create problems with decoration + List allProblems = createProblemDescriptorsWithDecoration(helperWithIssues); + + if (allProblems.isEmpty()) { + CxLogger.info(LOG_TAG + " No problem descriptors created for: " + + problemHelper.getFile().getName()); + return new ProblemDescriptor[0]; + } + + // Cache problem descriptors + helperWithIssues.getProblemHolderService().addProblemDescriptors( + problemHelper.getFilePath(), allProblems); + + CxLogger.info(LOG_TAG + " Created " + allProblems.size() + + " problem descriptors for: " + problemHelper.getFile().getName()); + + return allProblems.toArray(new ProblemDescriptor[0]); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error during scan: " + e.getMessage(), e); + return new ProblemDescriptor[0]; + } + } + + /** + * Create problem descriptors with UI decoration. + * + * Removes existing annotations, validates issues, creates descriptors, + * and decorates the editor with visual feedback. + * + * @param problemHelper Helper with scan issues + * @return List of created problem descriptors + */ + private List createProblemDescriptorsWithDecoration( + ProblemHelper problemHelper) { + + if (isScanIssuePresent(problemHelper.getScanIssueList())) { + // Clear existing decorations + ProblemDecorator.removeAllHighlighters(problemHelper.getProject()); + + // Process issues with decoration enabled + List descriptors = createProblemDescriptors( + problemHelper, true); + + // Decorate UI + if (!descriptors.isEmpty()) { + decorateUI(problemHelper.getDocument(), problemHelper.getFile(), + problemHelper.getScanIssueList()); + } + + return descriptors; + } + return Collections.emptyList(); + } + + /** + * Create problem descriptors without UI decoration. + * + * @param problemHelper Helper with scan issues + * @return List of created problem descriptors + */ + public List createProblemDescriptorsWithoutDecoration( + ProblemHelper problemHelper) { + + if (isScanIssuePresent(problemHelper.getScanIssueList())) { + return createProblemDescriptors(problemHelper, false); + } + return Collections.emptyList(); + } + + /** + * Create problem descriptors from scan issues. + * + * For each scan issue: + * 1. Create ScanIssueProcessor + * 2. Validate and create ProblemDescriptor + * 3. Collect non-null descriptors + * + * @param problemHelper Helper with context and issues + * @param isDecoratorEnabled Whether to enable visual decoration + * @return List of valid problem descriptors + */ + private List createProblemDescriptors( + ProblemHelper problemHelper, + boolean isDecoratorEnabled) { + + List descriptors = new ArrayList<>(); + ScanIssueProcessor processor = new ScanIssueProcessor(problemHelper); + + for (ScanIssue scanIssue : problemHelper.getScanIssueList()) { + ProblemDescriptor descriptor = processor.processScanIssue( + scanIssue, isDecoratorEnabled); + if (descriptor != null) { + descriptors.add(descriptor); + } + } + + CxLogger.info(LOG_TAG + " Created " + descriptors.size() + + " problem descriptors from " + problemHelper.getScanIssueList().size() + + " scan issues"); + + return descriptors; + } + + /** + * Get existing problem descriptors for a file. + * + * Called when file hasn't changed since last scan. + * Returns cached problem descriptors. + * + * @param problemHolderService Cache service + * @param filePath File path + * @param document Document (for validation) + * @param file IFile + * @param supportedEnabledScanners Enabled scanners + * @return Array of cached problem descriptors + */ + public ProblemDescriptor[] getExistingProblems( + ProblemHolderService problemHolderService, + String filePath, + IDocument document, + IFile file, + List supportedEnabledScanners) { + + ProblemHelper problemHelper = ProblemHelper.builder(file, file.getProject()) + .filePath(filePath) + .document(document) + .supportedScanners(supportedEnabledScanners) + .problemHolderService(problemHolderService) + .problemDecorator(this.problemDecorator) + .build(); + + // Get cached issues + List scanIssueList = problemHolderService.getScanIssuesByFile(filePath); + if (scanIssueList.isEmpty()) { + CxLogger.warning(LOG_TAG + " No cached issues for: " + filePath); + resetEditorAndResults(file.getProject(), filePath); + decorateUIForIgnoreVulnerability(file, scanIssueList); + return new ProblemDescriptor[0]; + } + + // Get cached problem descriptors + List cachedDescriptors = problemHolderService.getProblemDescriptors(filePath); + if (cachedDescriptors.isEmpty()) { + CxLogger.warning(LOG_TAG + " No cached problem descriptors for: " + filePath); + decorateUIForIgnoreVulnerability(file, scanIssueList); + return new ProblemDescriptor[0]; + } + + // Decorate UI with cached issues + decorateUI(document, file, scanIssueList); + + CxLogger.info(LOG_TAG + " Returning " + cachedDescriptors.size() + + " cached problem descriptors for: " + file.getName()); + + return cachedDescriptors.toArray(new ProblemDescriptor[0]); + } + + /** + * Decorate UI with scan results (gutter icons, underlines). + * + * @param document Document to decorate + * @param file File being decorated + * @param scanIssueList Issues to show + */ + public void decorateUI(IDocument document, IFile file, List scanIssueList) { + try { + ProblemDecorator.decorateEditor(file, scanIssueList); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error decorating UI: " + e.getMessage(), e); + } + } + + /** + * Decorate UI for ignored vulnerabilities (empty if none ignored). + * + * @param file File to decorate + * @param scanIssueList Issues (may be empty) + */ + public void decorateUIForIgnoreVulnerability(IFile file, List scanIssueList) { + try { + CxLogger.info(LOG_TAG + " decorateUIForIgnoreVulnerability called for: " + file.getName()); + // TODO: Integrate with IgnoredProblemsStore when available + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error in decorateUIForIgnoreVulnerability: " + e.getMessage(), e); + } + } + + /** + * Reset editor and clear all cached results for a file. + * + * Called when: + * - File is closed + * - Scan encounters error + * - User requests reset + * + * @param project Project containing file + * @param filePath File path to reset + */ + public void resetEditorAndResults(IProject project, String filePath) { + try { + if (project == null || !project.isOpen()) { + return; + } + + // Clear visual decorations + ProblemDecorator.removeAllHighlighters(project); + + // Clear cached data + ProblemHolderService problemHolderService = ProblemHolderService.getInstance(project); + if (problemHolderService != null && filePath != null && !filePath.isEmpty()) { + problemHolderService.removeProblemDescriptorsForFile(filePath); + problemHolderService.removeScanIssues(filePath); + } + + CxLogger.info(LOG_TAG + " Reset editor and results for: " + filePath); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error resetting: " + e.getMessage(), e); + } + } + + /** + * Check if scan issues are present. + * + * @param scanIssueList List to check + * @return true if not null and not empty + */ + private boolean isScanIssuePresent(List scanIssueList) { + return scanIssueList != null && !scanIssueList.isEmpty(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java new file mode 100644 index 00000000..1e4b3930 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java @@ -0,0 +1,188 @@ +package com.checkmarx.eclipse.devassist.inspection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; + +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; +import com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Scheduler that wraps and coordinates RealTimeScanJob for background file scanning. + * + * Responsibilities: + * - Manage scheduling of real-time scans with debounce + * - Track pending scans per file + * - Cancel pending scans when needed + * - Provide clean API for scan orchestration + * + * Wraps Eclipse RealTimeScanJob which extends Job for background execution. + */ +public class DevAssistScanScheduler { + + private static final String LOG_TAG = "[SCAN-SCHEDULER]"; + private static final long DEFAULT_DEBOUNCE_DELAY_MS = 1000L; + + // Track pending jobs per file path + private final Map pendingScans = new ConcurrentHashMap<>(); + + /** + * Schedule a scan for a file with default debounce delay (1 second). + * + * If a scan is already pending for this file, returns false. + * Use reschedule() to cancel and restart with new delay. + * + * @param file File to scan + * @param problemHelper Problem context (unused in current impl, for alignment) + * @return true if scheduled, false if already pending + */ + public boolean scheduleInspection(IFile file, ProblemHelper problemHelper) { + return scheduleInspection(file, DEFAULT_DEBOUNCE_DELAY_MS); + } + + /** + * Schedule a scan for a file with custom debounce delay. + * + * @param file File to scan + * @param delayMs Debounce delay in milliseconds + * @return true if scheduled, false if already pending + */ + public boolean scheduleInspection(IFile file, long delayMs) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + + // Check if already pending + if (pendingScans.containsKey(filePath)) { + CxLogger.info(LOG_TAG + " Scan already pending for: " + filePath); + return false; + } + + try { + // Create new job + RealTimeScanJob scanJob = new RealTimeScanJob(file, file.getName()); + + // Track it + pendingScans.put(filePath, scanJob); + + // Schedule with debounce delay + scanJob.schedule(delayMs); + + CxLogger.info(LOG_TAG + " Scheduled scan for: " + filePath + + " (delay=" + delayMs + "ms)"); + return true; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to schedule scan: " + e.getMessage(), e); + pendingScans.remove(filePath); + return false; + } + } + + /** + * Reschedule a pending scan (cancel current, start new with delay). + * + * Used by CheckmarxDocumentListener when user types: + * - First keystroke: schedule with 1s delay + * - While typing: reschedule (cancel, start new 1s timer) + * - After user pauses: job runs + * + * @param file File to reschedule + * @param delayMs New debounce delay + * @return true if rescheduled, false if no pending job + */ + public boolean rescheduleInspection(IFile file, long delayMs) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + RealTimeScanJob existingJob = pendingScans.get(filePath); + + if (existingJob == null) { + // No pending job, schedule new one + return scheduleInspection(file, delayMs); + } + + try { + // Cancel current + existingJob.cancel(); + + // Reschedule with new delay + existingJob.reschedule(delayMs); + + CxLogger.info(LOG_TAG + " Rescheduled scan for: " + filePath + + " (delay=" + delayMs + "ms)"); + return true; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to reschedule: " + e.getMessage(), e); + return false; + } + } + + /** + * Cancel a pending scan for a file. + * + * @param file File to cancel scan for + * @return true if cancelled, false if no pending scan + */ + public boolean cancelScheduledInspection(IFile file) { + if (file == null) { + return false; + } + + String filePath = file.getLocation().toOSString(); + RealTimeScanJob job = pendingScans.remove(filePath); + + if (job == null) { + return false; + } + + try { + job.cancel(); + CxLogger.info(LOG_TAG + " Cancelled scan for: " + filePath); + return true; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error cancelling scan: " + e.getMessage(), e); + return false; + } + } + + /** + * Trigger inspection on the entire project (force re-inspection). + * + * @param project Project to inspect + */ + public void triggerInspection(IProject project) { + if (project == null) { + return; + } + CxLogger.info(LOG_TAG + " Triggering inspection for project: " + project.getName()); + // Future: force re-inspect all files in project + } + + /** + * Get number of pending scans. + * + * @return Count of scheduled but not yet running scans + */ + public int getPendingScansCount() { + return pendingScans.size(); + } + + /** + * Get statistics for debugging. + * + * @return Summary string + */ + public String getStatistics() { + return "Pending scans: " + pendingScans.size() + + ", Tracked files: " + pendingScans.keySet(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java new file mode 100644 index 00000000..f6fc6cfa --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java @@ -0,0 +1,45 @@ +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; + + public Location() { + } + + public Location(int line, int startIndex, int endIndex) { + this.line = line; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + 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; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java new file mode 100644 index 00000000..6b7d6fbb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanEngine.java @@ -0,0 +1,36 @@ +package com.checkmarx.eclipse.devassist.model; + +/** + * Enumeration of scan engines supported by Checkmarx. + */ +public enum ScanEngine { + ASCA("ASCA"), + OSS("OSS"), + SECRETS("SECRETS"), + CONTAINERS("CONTAINERS"), + IAC("IAC"); + + private final String displayName; + + ScanEngine(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } + + public static ScanEngine fromString(String value) { + for (ScanEngine engine : ScanEngine.values()) { + if (engine.displayName.equalsIgnoreCase(value)) { + return engine; + } + } + throw new IllegalArgumentException("Unknown scan engine: " + value); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java new file mode 100644 index 00000000..f72d961b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/ScanIssue.java @@ -0,0 +1,194 @@ +package com.checkmarx.eclipse.devassist.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a scan issue detected during a real-time scan. + * Captures detailed information about security issues identified in a scanned project. + * Each scan issue can have multiple locations and vulnerabilities. + */ +public class ScanIssue { + + private String scanIssueId; + private String severity; + private String title; + private String description; + private String remediationAdvise; + private String packageVersion; + private String packageManager; + private String cve; + private ScanEngine scanEngine; + private String filePath; + private String imageTag; + private String fileType; + private String secretValue; + private String similarityId; + private Integer ruleId; + private Integer problematicLineNumber; + private List locations = new ArrayList<>(); + private List vulnerabilities = new ArrayList<>(); + + public ScanIssue() { + } + + public ScanIssue(String scanIssueId, String severity, String title, String description, + String remediationAdvise, String packageVersion, String packageManager, String cve, + ScanEngine scanEngine, String filePath, String imageTag) { + this.scanIssueId = scanIssueId; + this.severity = severity; + this.title = title; + this.description = description; + this.remediationAdvise = remediationAdvise; + this.packageVersion = packageVersion; + this.packageManager = packageManager; + this.cve = cve; + this.scanEngine = scanEngine; + this.filePath = filePath; + this.imageTag = imageTag; + } + + public String getScanIssueId() { + return scanIssueId; + } + + public void setScanIssueId(String scanIssueId) { + this.scanIssueId = scanIssueId; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getRemediationAdvise() { + return remediationAdvise; + } + + public void setRemediationAdvise(String remediationAdvise) { + this.remediationAdvise = remediationAdvise; + } + + public String getPackageVersion() { + return packageVersion; + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + public String getPackageManager() { + return packageManager; + } + + public void setPackageManager(String packageManager) { + this.packageManager = packageManager; + } + + public String getCve() { + return cve; + } + + public void setCve(String cve) { + this.cve = cve; + } + + public ScanEngine getScanEngine() { + return scanEngine; + } + + public void setScanEngine(ScanEngine scanEngine) { + this.scanEngine = scanEngine; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public String getImageTag() { + return imageTag; + } + + public void setImageTag(String imageTag) { + this.imageTag = imageTag; + } + + public String getFileType() { + return fileType; + } + + public void setFileType(String fileType) { + this.fileType = fileType; + } + + public String getSecretValue() { + return secretValue; + } + + public void setSecretValue(String secretValue) { + this.secretValue = secretValue; + } + + public String getSimilarityId() { + return similarityId; + } + + public void setSimilarityId(String similarityId) { + this.similarityId = similarityId; + } + + public Integer getRuleId() { + return ruleId; + } + + public void setRuleId(Integer ruleId) { + this.ruleId = ruleId; + } + + public Integer getProblematicLineNumber() { + return problematicLineNumber; + } + + public void setProblematicLineNumber(Integer problematicLineNumber) { + this.problematicLineNumber = problematicLineNumber; + } + + public List getLocations() { + return locations; + } + + public void setLocations(List locations) { + this.locations = locations; + } + + public List getVulnerabilities() { + return vulnerabilities; + } + + public void setVulnerabilities(List vulnerabilities) { + this.vulnerabilities = vulnerabilities; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java new file mode 100644 index 00000000..d7e828ec --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Vulnerability.java @@ -0,0 +1,82 @@ +package com.checkmarx.eclipse.devassist.model; + +/** + * Represents a vulnerability associated with a scan issue. + * Provides additional insights into the security risk. + */ +public class Vulnerability { + + private String vulnerabilityId; + private String severity; + private String title; + private String description; + private String actualValue; + private String cve; + private String fixVersion; + + public Vulnerability() { + } + + public Vulnerability(String vulnerabilityId, String severity, String title, String description) { + this.vulnerabilityId = vulnerabilityId; + this.severity = severity; + this.title = title; + this.description = description; + } + + public String getVulnerabilityId() { + return vulnerabilityId; + } + + public void setVulnerabilityId(String vulnerabilityId) { + this.vulnerabilityId = vulnerabilityId; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getActualValue() { + return actualValue; + } + + public void setActualValue(String actualValue) { + this.actualValue = actualValue; + } + + public String getCve() { + return cve; + } + + public void setCve(String cve) { + this.cve = cve; + } + + public String getFixVersion() { + return fixVersion; + } + + public void setFixVersion(String fixVersion) { + this.fixVersion = fixVersion; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java new file mode 100644 index 00000000..e7defebc --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java @@ -0,0 +1,214 @@ +package com.checkmarx.eclipse.devassist.prefs; + +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import com.checkmarx.eclipse.Activator; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.*; + +/** + * 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 Label ascaInstallationMsg; + private Button ossCheckbox; + private Button secretsCheckbox; + private Button containersCheckbox; + private Button iacCheckbox; + private Combo containersToolCombo; + private Label mcpStatusLabel; + + 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 CheckmarxPreferencePage() { + super(); + setPreferenceStore(Activator.getDefault().getPreferenceStore()); + } + + @Override + protected Control createContents(Composite parent) { + Composite mainPanel = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.verticalSpacing = 8; + layout.horizontalSpacing = 0; + mainPanel.setLayout(layout); + mainPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // Assist Message Label (Hidden by default, red text) + assistMessageLabel = new Label(mainPanel, SWT.NONE); + assistMessageLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); + GridData msgData = new GridData(GridData.FILL_HORIZONTAL); + msgData.exclude = true; // Equivalent to hidemode 3 + assistMessageLabel.setLayoutData(msgData); + assistMessageLabel.setVisible(false); + + // --- ASCA Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); + Composite ascaComp = createIndentComposite(mainPanel); + ascaCheckbox = new Button(ascaComp, SWT.CHECK); + ascaCheckbox.setText("Enable ASCA Scanner"); + + // --- OSS Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); + Composite ossComp = createIndentComposite(mainPanel); + ossCheckbox = new Button(ossComp, SWT.CHECK); + ossCheckbox.setText("Enable OSS Scanner"); + + // --- Secrets Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); + Composite secretsComp = createIndentComposite(mainPanel); + secretsCheckbox = new Button(secretsComp, SWT.CHECK); + secretsCheckbox.setText("Enable Secrets Scanner"); + + // --- Containers Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); + Composite containersComp = createIndentComposite(mainPanel); + containersCheckbox = new Button(containersComp, SWT.CHECK); + containersCheckbox.setText("Enable Container Scanner"); + + // --- IaC Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); + Composite iacComp = createIndentComposite(mainPanel); + iacCheckbox = new Button(iacComp, SWT.CHECK); + iacCheckbox.setText("Enable IaC Scanner"); + + // --- Container Tool Selection Section --- + createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); + Composite containerToolComp = createIndentComposite(mainPanel); + Label containerDesc = new Label(containerToolComp, SWT.WRAP); + containerDesc.setText(CONTAINERS_TOOL_DESCRIPTION); + GridData descData = new GridData(GridData.FILL_HORIZONTAL); + containerDesc.setLayoutData(descData); + + containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); + containersToolCombo.setItems(new String[] { "docker", "podman"}); + containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + loadValues(); + return mainPanel; + } + + 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() { + 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() { + // Save current UI control state into PreferenceStore + IPreferenceStore store = getPreferenceStore(); + store.setValue(PREF_ASCA_ENABLED, ascaCheckbox.getSelection()); + store.setValue(PREF_OSS_ENABLED, ossCheckbox.getSelection()); + store.setValue(PREF_SECRETS_ENABLED, secretsCheckbox.getSelection()); + store.setValue(PREF_CONTAINERS_ENABLED, containersCheckbox.getSelection()); + store.setValue(PREF_IAC_ENABLED, iacCheckbox.getSelection()); + + if (containersToolCombo.getText() != null) { + store.setValue(PREF_CONTAINERS_TOOL, containersToolCombo.getText()); + } + + // Trigger change event for listeners + store.firePropertyChangeEvent("scannerPreferencesChanged", null, null); + + return super.performOk(); + } + +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java new file mode 100644 index 00000000..8b047444 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemBuilder.java @@ -0,0 +1,103 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.ArrayList; +import java.util.List; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Static factory for creating ProblemDescriptor objects. + * + * Encapsulates logic for: + * - Formatting problem descriptions + * - Creating appropriate fixes for issues + * - Building ProblemDescriptor instances + * + * Mirrors JetBrains ProblemBuilder. + * Cannot be instantiated. + */ +public final class ProblemBuilder { + + private ProblemBuilder() { + } + + /** + * Build a ProblemDescriptor from a scan issue. + * + * Mirrors JetBrains ProblemBuilder.build(). + * + * @param problemHelper Context with file, document, etc. + * @param scanIssue The scan issue to describe + * @param problemLineNumber Line number where problem was found + * @return ProblemDescriptor with formatted description and fixes + */ + public static ProblemDescriptor build( + ProblemHelper problemHelper, + ScanIssue scanIssue, + int problemLineNumber) { + + String description = formatDescription(scanIssue); + List fixes = createFixes(scanIssue); + + return ProblemDescriptor.builder() + .file(problemHelper.getFile()) + .scanIssue(scanIssue) + .lineNumber(problemLineNumber) + .description(description) + .fixes(fixes) + .build(); + } + + /** + * Format the problem description from scan issue details. + * + * @param scanIssue The scan issue + * @return HTML-formatted description for display + */ + private static String formatDescription(ScanIssue scanIssue) { + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append("").append(escapeHtml(scanIssue.getTitle())).append(""); + sb.append("
"); + sb.append("Severity: ").append(scanIssue.getSeverity()); + sb.append("
"); + if (scanIssue.getDescription() != null && !scanIssue.getDescription().isEmpty()) { + sb.append(escapeHtml(scanIssue.getDescription())); + } + sb.append(""); + return sb.toString(); + } + + /** + * Escape HTML special characters for safe display. + * + * @param text Text to escape + * @return HTML-escaped text + */ + private static String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + /** + * Create fixes for a scan issue. + * + * Currently creates: ViewDetailsFix + * Can be extended with: IgnoreVulnerabilityFix, etc. + * + * @param scanIssue The scan issue + * @return List of fixes (currently all as Object, can be typed later) + */ + private static List createFixes(ScanIssue scanIssue) { + List fixes = new ArrayList<>(); + // Future: add ViewDetailsFix, IgnoreVulnerabilityFix, etc. + return fixes; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java new file mode 100644 index 00000000..fdc58e84 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -0,0 +1,641 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Renders scan results as editor decorations. + * + * Creates visual indicators for issues in the editor: + * - Gutter icons (severity indicators on line numbers) + * - Line highlighting (background color by severity) + * - Annotations (squiggly underlines and tooltips) + * + * Integrates with Eclipse's SourceViewerConfiguration to display + * issue markers alongside the editor content. + */ +public class ProblemDecorator { + + private static final String LOG_TAG = "[SCAN-DECORATOR]"; + + // Track annotations we've created so we can remove them later + private static final Map> fileAnnotations = + new HashMap<>(); + + /** + * Render scan results as annotations in the editor. + * + * Creates FindingsAnnotation objects for each issue and adds them + * to the editor's annotation model for visual display. + * + * @param file File that was scanned + * @param scanIssues Issues to visualize + */ + public static void decorateEditor(IFile file, List scanIssues) { + System.out.println("[SCAN-DECORATOR-ENTRY] decorateEditor called with " + + (scanIssues != null ? scanIssues.size() : "null") + " issues"); + + if (file == null) { + return; + } + if (scanIssues == null) { + return; + } + if (scanIssues.isEmpty()) { + return; + } + + // **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(); + System.out.println("[SCAN-DECORATOR-ENTRY] File path: " + filePath); + + try { + // Find open editor for this file + System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 1/3] Finding open editor..."); + ITextEditor editor = findOpenEditor(file); + if (editor == null) { + System.out.println("[SCAN-DECORATOR-ENTRY] ✗ [STEP 1/3] No open editor for: " + filePath); + CxLogger.info(LOG_TAG + " ✗ No open editor for: " + filePath); + return; + } + System.out.println("[SCAN-DECORATOR-ENTRY] ✓ [STEP 1/3] Found editor: " + editor.getClass().getSimpleName()); + + // Get annotation model from editor + System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 2/3] Getting annotation model..."); + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + + if (annotationModel == null) { + System.out.println("[SCAN-DECORATOR-ENTRY] ✗ [STEP 2/3] Annotation model is NULL"); + CxLogger.warning(LOG_TAG + " ✗ No annotation model available"); + return; + } + System.out.println("[SCAN-DECORATOR-ENTRY] ✓ [STEP 2/3] Got annotation model"); + + // Remove previous annotations for this file + System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 3/3] Processing " + scanIssues.size() + " issues..."); + clearAnnotations(filePath, annotationModel); + + // Add new annotations for each issue + List annotations = new java.util.ArrayList<>(); + + for (ScanIssue issue : scanIssues) { + try { + FindingsAnnotation annotation = createAnnotation(editor, issue); + if (annotation != null) { + annotation.addButton(filePath, null); + annotations.add(annotation); + + CxLogger.info(LOG_TAG + " ─────────────────────────────────────────────────"); + CxLogger.info(LOG_TAG + " Issue: " + issue.getTitle()); + CxLogger.info(LOG_TAG + " Engine: " + issue.getScanEngine()); + CxLogger.info(LOG_TAG + " Severity: " + issue.getSeverity()); + + // **OSS-SPECIFIC LOGIC: Only decorate the first line (used for redirection)** + // For OSS issues, decorate only the first location's line to keep it simple + org.eclipse.jface.text.Position pos = null; + + if (issue.getScanEngine() != null && + issue.getScanEngine().name().equalsIgnoreCase("OSS")) { + // OSS: Decorate only the first line where package is declared + pos = decorateOssFirstLineOnly(editor, issue); + } else { + // Other engines: Use standard range calculation + pos = calculateRange(editor, issue); + } + + if (pos != null && pos.getLength() > 0) { + CxLogger.info(LOG_TAG + " Calculated Position for Editor:"); + CxLogger.info(LOG_TAG + " Offset: " + pos.getOffset()); + CxLogger.info(LOG_TAG + " Length: " + pos.getLength()); + CxLogger.info(LOG_TAG + " Range: [" + pos.getOffset() + "-" + (pos.getOffset() + pos.getLength()) + "]"); + + // Add annotation to model for display + annotationModel.addAnnotation(annotation, pos); + CxLogger.info(LOG_TAG + " ✓ Annotation added to model"); + } else { + CxLogger.warning(LOG_TAG + " ✗ FAILED: Invalid position (offset=" + + (pos != null ? pos.getOffset() : "null") + ", length=" + + (pos != null ? pos.getLength() : "null") + ")"); + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating annotation: " + + e.getMessage()); + e.printStackTrace(); + } + } + + // Store annotations for later cleanup + fileAnnotations.put(filePath, annotations); + + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + CxLogger.info(LOG_TAG + " ✓ COMPLETE: Added " + annotations.size() + + " annotations to editor"); + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error decorating editor: " + + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Create a FindingsAnnotation for a scan issue. + * + * FindingsAnnotation extends Eclipse's Annotation class and provides + * custom rendering (color, icon, tooltip) based on issue severity. + * + * @param editor Text editor + * @param issue Scan issue + * @return FindingsAnnotation, or null if creation fails + */ + private static FindingsAnnotation createAnnotation(ITextEditor editor, + ScanIssue issue) { + try { + // Get severity from issue + String severity = issue.getSeverity(); + + // DEBUG: Log the actual severity value + CxLogger.info(LOG_TAG + " [DEBUG] Issue: " + issue.getTitle() + + " | Severity from issue: " + (severity != null ? severity : "NULL")); + + // Map severity to annotation type + String annotationType = mapSeverityToAnnotationType(severity); + + CxLogger.info(LOG_TAG + " [DEBUG] Mapped to annotation type: " + annotationType); + + // Create annotation with issue details + FindingsAnnotation annotation = new FindingsAnnotation( + annotationType, + issue.getTitle(), + issue.getDescription() + ); + return annotation; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating annotation: " + + e.getMessage()); + return null; + } + } + + + /** + * Map severity level to custom Findings annotation type. + * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. + * + * @param severity Severity string (MALICIOUS, CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN, OK, IGNORED) + * @return Annotation type constant (com.checkmarx.eclipse.findings.{severity}) + */ + private static String mapSeverityToAnnotationType(String severity) { + if (severity == null) { + return "com.checkmarx.eclipse.findings.unknown"; + } + String upper = severity.toUpperCase(); + if (upper.contains("MALICIOUS")) { + return "com.checkmarx.eclipse.findings.malicious"; + } + if (upper.contains("CRITICAL") || upper.contains("ERROR")) { + return "com.checkmarx.eclipse.findings.critical"; + } + if (upper.contains("HIGH")) { + return "com.checkmarx.eclipse.findings.high"; + } + if (upper.contains("MEDIUM")) { + return "com.checkmarx.eclipse.findings.medium"; + } + if (upper.contains("LOW") || upper.contains("INFO")) { + return "com.checkmarx.eclipse.findings.low"; + } + if (upper.contains("UNKNOWN")) { + return "com.checkmarx.eclipse.findings.unknown"; + } + if (upper.contains("OK")) { + return "com.checkmarx.eclipse.findings.ok"; + } + if (upper.contains("IGNORED")) { + return "com.checkmarx.eclipse.findings.ignored"; + } + + return "com.checkmarx.eclipse.findings.unknown"; + } + + /** + * Decorate only the first line for OSS issues (package declaration line). + * + * For OSS vulnerabilities, the Location has the exact character range, + * but it may span the entire dependency block. We simplify by decorating + * only the first line where the package is declared. + * + * @param editor Text editor + * @param issue OSS issue + * @return Position covering the entire first line, or null if unable to determine + */ + private static org.eclipse.jface.text.Position decorateOssFirstLineOnly( + ITextEditor editor, ScanIssue issue) { + + try { + org.eclipse.jface.text.IDocument document = + editor.getDocumentProvider().getDocument(editor.getEditorInput()); + + if (document == null) { + CxLogger.warning(LOG_TAG + " [OSS] Document is null!"); + return null; + } + + // Get the line number from first location + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + CxLogger.warning(LOG_TAG + " [OSS] No locations found!"); + return null; + } + + com.checkmarx.eclipse.devassist.model.Location location = + issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // Convert to 0-based + + int docLength = document.getLength(); + int lineCount = document.getNumberOfLines(); + + // Bounds check + if (lineNumber < 0 || lineNumber >= lineCount) { + CxLogger.warning(LOG_TAG + " [OSS] Line " + (lineNumber + 1) + + " out of bounds (doc has " + lineCount + " lines)"); + return null; + } + + // Get the entire line information + org.eclipse.jface.text.IRegion lineInfo = document.getLineInformation(lineNumber); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + + // For OSS, decorate the entire line (excluding trailing newline) + // This ensures the gutter icon and underline appear on the whole line + int decorationLength = lineLength; + if (decorationLength == 0) { + decorationLength = 1; // Minimum 1 char + } + + CxLogger.info(LOG_TAG + " [OSS] Line " + (lineNumber + 1) + + " (offset=" + lineOffset + ", length=" + decorationLength + ")"); + + // Bounds check final position + if (lineOffset < 0 || lineOffset > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Line offset " + lineOffset + + " out of bounds (doc length=" + docLength + ")"); + return null; + } + + if (lineOffset + decorationLength > docLength) { + CxLogger.warning(LOG_TAG + " [OSS] Adjusted length from " + + decorationLength + " to " + (docLength - lineOffset)); + decorationLength = docLength - lineOffset; + } + + if (decorationLength <= 0) { + CxLogger.warning(LOG_TAG + " [OSS] Invalid decoration length: " + decorationLength); + return null; + } + + CxLogger.info(LOG_TAG + " [OSS] ✓ Decorating first line: [" + lineOffset + + "-" + (lineOffset + decorationLength) + "] = " + decorationLength + " chars"); + + return new org.eclipse.jface.text.Position(lineOffset, decorationLength); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " [OSS] Error decorating first line: " + + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * Calculate the precise source range for an annotation. + * + * Handles BOTH absolute and line-relative offsets depending on scanner: + * - Secrets API: Returns RealtimeLocation with ABSOLUTE document offsets + * - ASCA API: Returns character positions that are LINE-RELATIVE offsets + * + * @param editor Text editor + * @param issue Scan issue with location info + * @return org.eclipse.jface.text.Position representing the precise range + */ + private static org.eclipse.jface.text.Position calculateRange(ITextEditor editor, ScanIssue issue) { + try { + IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + if (document == null) return new org.eclipse.jface.text.Position(0, 1); + + int docLength = document.getLength(); + + // 1. Precise location-based offset calculation + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location location = issue.getLocations().get(0); + int rawStart = location.getStartIndex(); + int rawEnd = location.getEndIndex(); + int line = Math.max(0, location.getLine() - 1); + + IRegion lineInfo = document.getLineInformation(line); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + + int trimIndent = getLeadingWhitespaceOffset(document, lineOffset, lineLength); + boolean isLineRelative = (rawStart == 0 && line > 0) || (rawEnd < 100 && rawEnd - rawStart < 100); + + int charStart = isLineRelative ? (lineOffset + rawStart) : rawStart; + int charEnd = isLineRelative ? (lineOffset + rawEnd) : rawEnd; + + // If start points to the beginning of the line, shift past leading whitespace + if (charStart <= lineOffset) { + charStart = lineOffset + trimIndent; + } + + if (charEnd <= charStart) { + charEnd = lineOffset + lineLength; + } + + // Clamp offsets safely within document bounds + charStart = Math.max(0, Math.min(charStart, docLength)); + charEnd = Math.max(charStart, Math.min(charEnd, docLength)); + + if (charEnd > charStart) { + return new org.eclipse.jface.text.Position(charStart, charEnd - charStart); + } + } + + // 2. Fallback: Highlight line content (skipping leading indentation) + int targetLine = 0; + if (issue.getProblematicLineNumber() != null) { + targetLine = issue.getProblematicLineNumber() - 1; + } else if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + targetLine = issue.getLocations().get(0).getLine() - 1; + } + + int line = Math.max(0, Math.min(targetLine, document.getNumberOfLines() - 1)); + IRegion lineInfo = document.getLineInformation(line); + + int trimIndent = getLeadingWhitespaceOffset(document, lineInfo.getOffset(), lineInfo.getLength()); + int startOffset = lineInfo.getOffset() + trimIndent; + int length = Math.max(1, lineInfo.getLength() - trimIndent); + + return new org.eclipse.jface.text.Position(startOffset, length); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error calculating range: " + e.getMessage()); + return new org.eclipse.jface.text.Position(0, 1); + } + } + /** + * Calculates the number of leading whitespace characters (spaces/tabs) on a given line. + * + * @param document Text document + * @param lineOffset Start character offset of the line + * @param lineLength Total length of the line + * @return Number of leading whitespace characters + */ + private static int getLeadingWhitespaceOffset(org.eclipse.jface.text.IDocument document, + int lineOffset, + int lineLength) { + try { + String lineText = document.get(lineOffset, lineLength); + int leadingSpaces = 0; + + while (leadingSpaces < lineText.length() && + Character.isWhitespace(lineText.charAt(leadingSpaces))) { + leadingSpaces++; + } + + return leadingSpaces; + } catch (Exception e) { + return 0; + } + } + + /** + * Clear previous annotations for a file. + * + * @param filePath File path + * @param annotationModel Annotation model + */ + private static void clearAnnotations(String filePath, + IAnnotationModel annotationModel) { + + try { + List previousAnnotations = fileAnnotations.get(filePath); + if (previousAnnotations != null) { + for (Annotation annotation : previousAnnotations) { + annotationModel.removeAnnotation(annotation); + } + fileAnnotations.remove(filePath); + + CxLogger.info(LOG_TAG + " ✓ Cleared " + previousAnnotations.size() + + " previous annotations"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing annotations: " + + e.getMessage()); + } + } + + /** + * Find open text editor for a file. + * + * @param file File to find editor for + * @return ITextEditor or null + */ + private static ITextEditor findOpenEditor(IFile file) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + + IWorkbenchPage page = null; + try { + page = workbench.getActiveWorkbenchWindow().getActivePage(); + } catch (NullPointerException e) { + // Workbench window not available, try all windows + for (var window : workbench.getWorkbenchWindows()) { + page = window.getActivePage(); + if (page != null) break; + } + } + + if (page == null) { + return null; + } + + var editors = page.getEditors(); + for (var editor : editors) { + Object input = editor.getEditorInput(); + if (input instanceof org.eclipse.ui.IFileEditorInput) { + IFile editorFile = ((org.eclipse.ui.IFileEditorInput) input) + .getFile(); + if (editorFile.equals(file)) { + // Try method 1: Direct ITextEditor instance + if (editor instanceof ITextEditor) { + return (ITextEditor) editor; + } + + // Try method 2: ITextEditor adapter (for MavenPomEditor, etc.) + ITextEditor textEditor = editor.getAdapter(ITextEditor.class); + if (textEditor != null) { + return textEditor; + } + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error finding open editor: " + + e.getMessage()); + } + + return null; + } + + /** + * Remove all decorations for a file. + * + * Called when: + * - Results are cleared + * - File is closed + * - Editor is disposed + * + * @param file File to remove decorations from + */ + public static void clearDecorations(IFile file) { + try { + // **FIX: Use getLocation() (absolute path) for consistency with decorateEditor()** + // Ensures fileAnnotations map lookups use the same path format + String filePath = file.getLocation().toOSString(); + CxLogger.info(LOG_TAG + " Clearing decorations for: " + filePath); + + ITextEditor editor = findOpenEditor(file); + if (editor == null) { + fileAnnotations.remove(filePath); + return; + } + + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + + if (annotationModel != null) { + clearAnnotations(filePath, annotationModel); + } + + CxLogger.info(LOG_TAG + " ✓ Decorations cleared"); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing decorations: " + + e.getMessage()); + } + } + + /** + * Get decorator statistics. + * + * @return Summary string + */ + public static String getStatistics() { + int totalAnnotations = fileAnnotations.values().stream() + .mapToInt(List::size) + .sum(); + return "Decorated files: " + fileAnnotations.size() + + ", Total annotations: " + totalAnnotations; + } + + /** + * Highlight a line and add gutter icon for a problem. + * + * Called by ScanIssueProcessor during per-issue processing. + * Integrates with the existing decoration system. + * + * @param problemHelper Problem helper with context (currently unused, for JetBrains API alignment) + * @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) { + + try { + CxLogger.info(LOG_TAG + " highlightLineAddGutterIconForProblem called for line: " + + problemLineNumber + " issue: " + scanIssue.getTitle()); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error in highlightLineAddGutterIconForProblem: " + e.getMessage(), e); + } + } + + /** + * Remove all highlighters/decorations from a project. + * + * Called by DevAssistInspectionMgr when resetting editor state. + * Clears all tracked annotations across all files. + * + * @param project Project to clear (used for context, actual clearing is project-wide) + */ + public static void removeAllHighlighters(org.eclipse.core.resources.IProject project) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + CxLogger.info(LOG_TAG + " removeAllHighlighters: Workbench not available"); + return; + } + + for (org.eclipse.ui.IWorkbenchWindow window : workbench.getWorkbenchWindows()) { + for (IWorkbenchPage page : window.getPages()) { + for (org.eclipse.ui.IEditorReference ref : page.getEditorReferences()) { + try { + ITextEditor editor = (ITextEditor) ref.getEditor(false); + if (editor != null) { + IAnnotationModel annotationModel = + editor.getDocumentProvider().getAnnotationModel(editor.getEditorInput()); + if (annotationModel != null) { + for (List annotations : fileAnnotations.values()) { + for (Annotation ann : annotations) { + try { + annotationModel.removeAnnotation(ann); + } catch (Exception e) { + // Continue removing others + } + } + } + } + } + } catch (Exception e) { + // Continue with other editors + } + } + } + } + + fileAnnotations.clear(); + CxLogger.info(LOG_TAG + " Removed all highlighters for project: " + project.getName()); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error removing all highlighters: " + e.getMessage(), e); + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java new file mode 100644 index 00000000..dee7d2a3 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDescriptor.java @@ -0,0 +1,117 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.List; + +import org.eclipse.core.resources.IFile; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Eclipse equivalent of JetBrains ProblemDescriptor. + * + * Represents a detected problem/issue in a file with: + * - Issue metadata (file, scan issue, line number) + * - Human-readable description + * - Associated fixes for the problem + * + * Option B: Full structure with fixes list, mirroring JetBrains. + */ +public class ProblemDescriptor { + + private final IFile file; + private final ScanIssue scanIssue; + private final int lineNumber; + private final String description; + private final List fixes; + + /** + * Constructor for ProblemDescriptor. + * + * @param file The file being analyzed + * @param scanIssue The scan issue + * @param lineNumber Line number of the issue + * @param description Human-readable description + * @param fixes Associated fixes + */ + public ProblemDescriptor(IFile file, ScanIssue scanIssue, int lineNumber, + String description, List fixes) { + this.file = file; + this.scanIssue = scanIssue; + this.lineNumber = lineNumber; + this.description = description; + this.fixes = fixes; + } + + public IFile getFile() { + return file; + } + + public ScanIssue getScanIssue() { + return scanIssue; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getDescription() { + return description; + } + + public List getFixes() { + return fixes; + } + + /** + * Get the problem fixes as an array. + * + * @return Array of fixes (or empty array if none) + */ + public Object[] getFixesArray() { + return fixes != null ? fixes.toArray() : new Object[0]; + } + + /** + * Builder for ProblemDescriptor. + */ + public static class Builder { + private IFile file; + private ScanIssue scanIssue; + private int lineNumber; + private String description; + private List fixes; + + public Builder file(IFile file) { + this.file = file; + return this; + } + + public Builder scanIssue(ScanIssue scanIssue) { + this.scanIssue = scanIssue; + return this; + } + + public Builder lineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder fixes(List fixes) { + this.fixes = fixes; + return this; + } + + public ProblemDescriptor build() { + return new ProblemDescriptor(file, scanIssue, lineNumber, description, fixes); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java new file mode 100644 index 00000000..19e9717b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHelper.java @@ -0,0 +1,174 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Helper class that aggregates all context needed for problem processing. + * + * Holds: file, project, document, scanners, issues, holder service, decorator. + * Used by orchestration flow to pass context to various processing stages. + * + * Mirrors JetBrains ProblemHelper with Eclipse types. + */ +public class ProblemHelper { + + private final IFile file; + private final IProject project; + private final String filePath; + private final IDocument document; + private final List supportedScanners; + private final List scanIssueList; + private final ProblemHolderService problemHolderService; + private final ProblemDecorator problemDecorator; + + /** + * Constructor for ProblemHelper. + */ + public ProblemHelper(IFile file, IProject project, String filePath, + IDocument document, List supportedScanners, + List scanIssueList, ProblemHolderService problemHolderService, + ProblemDecorator problemDecorator) { + this.file = file; + this.project = project; + this.filePath = filePath; + this.document = document; + this.supportedScanners = supportedScanners; + this.scanIssueList = scanIssueList; + this.problemHolderService = problemHolderService; + this.problemDecorator = problemDecorator; + } + + public IFile getFile() { + return file; + } + + public IProject getProject() { + return project; + } + + public String getFilePath() { + return filePath; + } + + public IDocument getDocument() { + return document; + } + + public List getSupportedScanners() { + return supportedScanners; + } + + public List getScanIssueList() { + return scanIssueList; + } + + public ProblemHolderService getProblemHolderService() { + return problemHolderService; + } + + public ProblemDecorator getProblemDecorator() { + return problemDecorator; + } + + /** + * Builder method enforcing mandatory fields: file, project. + * + * Mirrors JetBrains ProblemHelper.builder(PsiFile, Project). + * + * @param file IFile to process + * @param project IProject containing the file + * @return Builder with file and project set + * @throws IllegalArgumentException if file or project is null + */ + public static Builder builder(IFile file, IProject project) { + if (Objects.isNull(file) || Objects.isNull(project)) { + throw new IllegalArgumentException( + "Mandatory fields required: file, project"); + } + return new Builder() + .file(file) + .project(project); + } + + /** + * Create a new builder from this ProblemHelper. + * + * @return Builder with all fields from this instance + */ + public Builder toBuilder() { + return builder(this.file, this.project) + .filePath(this.filePath) + .document(this.document) + .supportedScanners(this.supportedScanners) + .scanIssueList(this.scanIssueList) + .problemHolderService(this.problemHolderService) + .problemDecorator(this.problemDecorator); + } + + /** + * Builder for ProblemHelper. + */ + public static class Builder { + private IFile file; + private IProject project; + private String filePath; + private IDocument document; + private List supportedScanners; + private List scanIssueList; + private ProblemHolderService problemHolderService; + private ProblemDecorator problemDecorator; + + public Builder file(IFile file) { + this.file = file; + return this; + } + + public Builder project(IProject project) { + this.project = project; + return this; + } + + public Builder filePath(String filePath) { + this.filePath = filePath; + return this; + } + + public Builder document(IDocument document) { + this.document = document; + return this; + } + + public Builder supportedScanners(List supportedScanners) { + this.supportedScanners = supportedScanners; + return this; + } + + public Builder scanIssueList(List scanIssueList) { + this.scanIssueList = scanIssueList; + return this; + } + + public Builder problemHolderService(ProblemHolderService problemHolderService) { + this.problemHolderService = problemHolderService; + return this; + } + + public Builder problemDecorator(ProblemDecorator problemDecorator) { + this.problemDecorator = problemDecorator; + return this; + } + + public ProblemHelper build() { + return new ProblemHelper(file, project, filePath, document, + supportedScanners, scanIssueList, problemHolderService, problemDecorator); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java new file mode 100644 index 00000000..42a2ff62 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -0,0 +1,262 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.e4.core.services.events.IEventBroker; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.utils.PluginUtils; + +/** + * In-memory cache for scan results (ScanIssue), keyed by file path. + * + * Thread-safe via ConcurrentHashMap. Used to avoid redundant scans + * and to enable result restoration when files are reopened. + * + * Mirrors JetBrains ProblemHolderService pattern with Eclipse IEventBroker for notifications. + */ +public class ProblemHolderService { + + private static final String LOG_TAG = "[PROBLEM-HOLDER]"; + public static final String ISSUES_UPDATED_TOPIC = "com/checkmarx/issues/updated"; + + // Session property key for storing service in project + public static final String SERVICE_KEY = ProblemHolderService.class.getName() + + ".INSTANCE"; + + private final ConcurrentHashMap> fileToScanIssues = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap> fileToProblemDescriptors = + new ConcurrentHashMap<>(); + + /** + * Returns the instance of this service for the given project. + * + * @param project the project. + * @return the instance of this service for the given project. + */ + public static ProblemHolderService getInstance(IProject project) { + return ProblemHolderService.getInstance(project); + } + + /** + * Cache scan issues for a file. + * + * @param filePath Absolute file path + * @param issues Issues found by scanners + */ + public void addScanIssues(String filePath, List issues) { + if (filePath == null || issues == null) { + return; + } + fileToScanIssues.put(filePath, new ArrayList<>(issues)); + // **KEY: Notify all listeners of the update (JetBrains pattern)** + publishIssuesUpdated(); + } + + /** + * Get cached scan issues for a file. + * + * @param filePath Absolute file path + * @return Cached issues or empty list + */ + public List getScanIssuesByFile(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + + List cached = fileToScanIssues.get(filePath); + return cached != null ? new ArrayList<>(cached) : Collections.emptyList(); + } + + /** + * Get all cached issues across all files. + * + * @return Map of file path → issues + */ + public Map> getAllScanIssues() { + + Map> result = new HashMap<>(); + for (Map.Entry> entry : fileToScanIssues.entrySet()) { + result.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + int totalIssues = result.values().stream().mapToInt(List::size).sum(); + return result; + } + + /** + * Merge new issues with existing issues for a file. + * Deduplicates by issue ID. + * + * @param filePath Absolute file path + * @param newIssues Issues to merge + */ + public void mergeScanIssues(String filePath, List newIssues) { + if (filePath == null || newIssues == null) { + return; + } + + List existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + Map merged = new HashMap<>(); + + // Add existing issues + for (ScanIssue issue : existing) { + merged.put(issue.getScanIssueId(), issue); + } + + // Add/override with new issues (by ID) + for (ScanIssue issue : newIssues) { + merged.put(issue.getScanIssueId(), issue); + } + + fileToScanIssues.put(filePath, new ArrayList<>(merged.values())); + CxLogger.info(LOG_TAG + " Merged " + newIssues.size() + " issues for: " + filePath); + + // **KEY: Notify listeners when cache is modified** + publishIssuesUpdated(); + } + + /** + * Clear cached issues for a file. + * + * @param filePath Absolute file path + */ + public void removeScanIssues(String filePath) { + if (filePath == null) { + return; + } + + fileToScanIssues.remove(filePath); + CxLogger.info(LOG_TAG + " Cleared cache for: " + filePath); + } + + /** + * Remove cached scan issues for a specific scanner type and file. + * Mirrors JetBrains DevAssistScanScheduler.cacheScanResults() pattern. + * + * When a partial re-scan is performed (e.g., only ASCA is rescanned), + * this method removes the old results for THAT scanner type before + * merging the new results. + * + * @param scannerType Name of the scanner engine (e.g., "ASCA", "OSS", "IaC") + * @param filePath Absolute file path + */ + public void removeScanIssuesByFileAndScanner(String scannerType, String filePath) { + if (filePath == null || scannerType == null) { + return; + } + + List existing = fileToScanIssues.getOrDefault(filePath, new ArrayList<>()); + List filtered = new ArrayList<>(); + + // Keep only issues from OTHER scanners + for (ScanIssue issue : existing) { + if (issue.getScanEngine() != null && + !issue.getScanEngine().name().equals(scannerType)) { + filtered.add(issue); + } + } + + fileToScanIssues.put(filePath, filtered); + CxLogger.info(LOG_TAG + " Removed " + scannerType + " issues for: " + filePath + + " (kept " + filtered.size() + " issues from other scanners)"); + } + + /** + * Clear all caches (on project close). + */ + public void clearAll() { + fileToScanIssues.clear(); + CxLogger.info(LOG_TAG + " All caches cleared"); + } + + /** + * Get cache statistics for debugging. + * + * @return Summary string + */ + public String getCacheStats() { + int fileCount = fileToScanIssues.size(); + int totalIssues = fileToScanIssues.values().stream() + .mapToInt(List::size) + .sum(); + return "Files: " + fileCount + ", Total Issues: " + totalIssues; + } + + /** + * Publish issues update via Eclipse IEventBroker. + * Subscribers listen on ISSUES_UPDATED_TOPIC using @UIEventTopic annotation. + * + * @see com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView + */ + private void publishIssuesUpdated() { + try { + IEventBroker eventBroker = PluginUtils.getEventBroker(); + if (eventBroker != null) { + Map> allIssues = getAllScanIssues(); + System.out.println(LOG_TAG + " [EVENT-BROKER] Publishing issues update: " + allIssues.size() + " files"); + eventBroker.post(ISSUES_UPDATED_TOPIC, allIssues); + } else { + System.err.println(LOG_TAG + " [EVENT-BROKER] ✗ EventBroker not available"); + } + } catch (Exception e) { + System.err.println(LOG_TAG + " [EVENT-BROKER] Error publishing event: " + e.getMessage()); + e.printStackTrace(); + } + } + + public static void addToCxOneFindings(IFile file, List problemsList) { + getInstance(file.getProject()).addScanIssues(file.getFullPath().toOSString(), problemsList); + } + + /** + * Cache problem descriptors for a file. + * + * @param filePath Absolute file path + * @param descriptors Problem descriptors to cache + */ + public void addProblemDescriptors(String filePath, List descriptors) { + if (filePath == null || descriptors == null) { + return; + } + fileToProblemDescriptors.put(filePath, new ArrayList<>(descriptors)); + CxLogger.info(LOG_TAG + " Cached " + descriptors.size() + " problem descriptors for: " + filePath); + } + + /** + * Get cached problem descriptors for a file. + * + * @param filePath Absolute file path + * @return Cached problem descriptors or empty list + */ + public List getProblemDescriptors(String filePath) { + if (filePath == null) { + return Collections.emptyList(); + } + List cached = fileToProblemDescriptors.get(filePath); + return cached != null ? Collections.unmodifiableList(cached) : Collections.emptyList(); + } + + /** + * Remove cached problem descriptors for a file. + * + * @param filePath Absolute file path + */ + public void removeProblemDescriptorsForFile(String filePath) { + if (filePath == null) { + return; + } + fileToProblemDescriptors.remove(filePath); + CxLogger.info(LOG_TAG + " Removed problem descriptors for: " + filePath); + } + +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java new file mode 100644 index 00000000..9581ff06 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java @@ -0,0 +1,221 @@ +package com.checkmarx.eclipse.devassist.problems; + +import java.util.Objects; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Processor that validates individual scan issues and creates problem descriptors. + * + * Encapsulates logic for: + * - Validating scan issue data (location, line, severity) + * - Creating problem descriptors for valid issues + * - Triggering decoration for highlighted issues + * + * CRITICAL: Prevents crashes from invalid data by validating before processing. + * + * Mirrors JetBrains ScanIssueProcessor. + */ +public class ScanIssueProcessor { + + private static final String LOG_TAG = "[SCAN-ISSUE-PROCESSOR]"; + + private final IFile file; + private final IDocument document; + private final ProblemHelper problemHelper; + + /** + * Constructor that takes file, document, and problemHelper. + * + * @param file The file being processed + * @param document The document + * @param problemHelper Problem helper with context + */ + public ScanIssueProcessor(IFile file, IDocument document, ProblemHelper problemHelper) { + this.file = file; + this.document = document; + this.problemHelper = problemHelper; + } + + /** + * Alternate constructor that extracts file and document from ProblemHelper. + * + * Mirrors JetBrains ScanIssueProcessor(ProblemHelper). + * + * @param problemHelper Problem helper containing file, document, etc. + */ + public ScanIssueProcessor(ProblemHelper problemHelper) { + this.file = problemHelper.getFile(); + this.document = problemHelper.getDocument(); + this.problemHelper = problemHelper; + } + + /** + * Process a single scan issue and create a problem descriptor if valid. + * + * Validation pipeline: + * 1. Check location exists and is not empty + * 2. Extract line number from location + * 3. Check line is within document range + * 4. Check severity is present and not blank + * 5. If all valid: create problem descriptor + * 6. If decorator enabled: highlight the issue + * + * Mirrors JetBrains ScanIssueProcessor.processScanIssue(). + * + * @param scanIssue Scan issue to process + * @param isDecoratorEnabled Whether to add visual decorations + * @return ProblemDescriptor if valid, null if invalid + */ + public ProblemDescriptor processScanIssue(ScanIssue scanIssue, boolean isDecoratorEnabled) { + + // Validation: location exists and is not empty + if (!isValidLocation(scanIssue)) { + CxLogger.info(LOG_TAG + " Invalid location for: " + scanIssue.getTitle()); + return null; + } + + // Extract line number + int problemLineNumber = scanIssue.getLocations().get(0).getLine(); + + // Validation: line number and severity are valid + if (!isValidLineAndSeverity(problemLineNumber, scanIssue)) { + CxLogger.info(LOG_TAG + " Invalid line/severity for: " + scanIssue.getTitle() + + " (line=" + problemLineNumber + ", severity=" + scanIssue.getSeverity() + ")"); + return null; + } + + try { + return processValidIssue(scanIssue, problemLineNumber, isDecoratorEnabled); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Exception processing issue: " + + scanIssue.getTitle() + ": " + e.getMessage(), e); + return null; + } + } + + /** + * Validate that scan issue has a location. + * + * @param scanIssue Scan issue to validate + * @return true if location exists and is not empty + */ + private boolean isValidLocation(ScanIssue scanIssue) { + return scanIssue.getLocations() != null && !scanIssue.getLocations().isEmpty(); + } + + /** + * Validate line number and severity. + * + * @param lineNumber Line number to check + * @param scanIssue Scan issue with severity + * @return true if line is in range and severity is not blank + */ + private boolean isValidLineAndSeverity(int lineNumber, ScanIssue scanIssue) { + // Check line is within document bounds + if (isLineOutOfRange(lineNumber)) { + return false; + } + // Check severity is present and not blank + return scanIssue.getSeverity() != null && !scanIssue.getSeverity().isBlank(); + } + + /** + * Check if line number is outside document range. + * + * @param lineNumber Line number to check + * @return true if line is out of range + */ + private boolean isLineOutOfRange(int lineNumber) { + try { + int lineCount = document.getNumberOfLines(); + return lineNumber < 1 || lineNumber > lineCount; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error checking line range: " + e.getMessage(), e); + return true; + } + } + + /** + * Process a valid scan issue. + * + * 1. Check if it's a "problem" (not just info/note) + * 2. If problem: create problem descriptor via ProblemBuilder + * 3. If decorator enabled: highlight the issue + * + * @param scanIssue The valid scan issue + * @param problemLineNumber Line number (already validated) + * @param isDecoratorEnabled Whether to decorate + * @return ProblemDescriptor if it's a problem, null if just info + */ + private ProblemDescriptor processValidIssue( + ScanIssue scanIssue, + int problemLineNumber, + boolean isDecoratorEnabled) { + + boolean isProblem = isProblem(scanIssue.getSeverity().toLowerCase()); + + ProblemDescriptor problemDescriptor = null; + if (isProblem) { + problemDescriptor = createProblemDescriptor(scanIssue, problemLineNumber); + } + + if (isDecoratorEnabled) { + highlightIssueIfNeeded(scanIssue, problemLineNumber, isProblem); + } + + return problemDescriptor; + } + + /** + * Check if severity indicates a reportable problem. + * + * @param severity Severity string (lowercase) + * @return true if problem, false if info/note + */ + private boolean isProblem(String severity) { + return severity.equals("critical") || + severity.equals("high") || + severity.equals("medium") || + severity.equals("low"); + } + + /** + * Create a problem descriptor via ProblemBuilder. + * + * @param scanIssue The scan issue + * @param problemLineNumber Line number + * @return ProblemDescriptor, or null on error + */ + private ProblemDescriptor createProblemDescriptor(ScanIssue scanIssue, int problemLineNumber) { + try { + return ProblemBuilder.build(problemHelper, scanIssue, problemLineNumber); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to create descriptor for: " + + scanIssue.getTitle() + ": " + e.getMessage(), e); + return null; + } + } + + /** + * Highlight the issue in the editor and add gutter icon. + * + * Delegates to ProblemDecorator to add visual decoration. + * + * @param scanIssue The scan issue + * @param problemLineNumber Line number + * @param isProblem Whether it's a problem or just note + */ + private void highlightIssueIfNeeded(ScanIssue scanIssue, int problemLineNumber, boolean isProblem) { + ProblemDecorator problemDecorator = problemHelper.getProblemDecorator(); + if (Objects.isNull(problemDecorator)) { + problemDecorator = new ProblemDecorator(); + } + problemDecorator.highlightLineAddGutterIconForProblem( + problemHelper, scanIssue, isProblem, problemLineNumber); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java new file mode 100644 index 00000000..5d6a49b6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java @@ -0,0 +1,272 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.ast.asca.ScanDetail; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Adapter class for handling ASCA scan results and converting them into a standardized format. + * + * This class wraps a ASCA {@link ScanResult} instance and provides methods to process and extract + * meaningful scan issues based on ASCA findings detected in the files. + * + * Features: + * - Groups multiple vulnerabilities on the same line + * - Sorts vulnerabilities by severity precedence + * - Filters ignored vulnerabilities (optional) + * - Generates proper unique IDs + * - Tracks location information + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class AscaScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[ASCA-ADAPTOR]"; + private static final String MULTIPLE_ISSUES_SUFFIX = " ASCA issues"; + + private final com.checkmarx.ast.asca.ScanResult ascaScanResult; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of AscaScanResultAdaptor with the specified ASCA scan results. + * + * @param ascaScanResult the ASCA scan results to be wrapped + * @param filePath the path of the file being scanned + */ + public AscaScanResultAdaptor(com.checkmarx.ast.asca.ScanResult ascaScanResult, String filePath) { + this.ascaScanResult = ascaScanResult; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public com.checkmarx.ast.asca.ScanResult getResults() { + return ascaScanResult; + } + + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Builds a list of ScanIssue objects from the ASCA scan results. + * Groups multiple vulnerabilities on the same line and sorts them by severity. + */ + private List buildIssues() { + if (ascaScanResult == null || ascaScanResult.getScanDetails() == null) { + CxLogger.info(LOG_TAG + " No scan results or scan details available"); + return Collections.emptyList(); + } + + List scanDetails = ascaScanResult.getScanDetails(); + if (scanDetails.isEmpty()) { + return Collections.emptyList(); + } + + // Group scan details by line number, then sort by severity precedence + Map> groupedIssues = scanDetails.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy( + ScanDetail::getLine, + Collectors.collectingAndThen(Collectors.toList(), detailsList -> { + detailsList.sort(Comparator.comparingInt(detail -> + getSeverityPrecedence(detail.getSeverity()))); + return detailsList; + }) + )); + + List issues = groupedIssues.values().stream() + .map(this::createScanIssueForGroup) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + issues.size() + " grouped scan issues for file: " + filePath); + return issues; + } + + /** + * Creates a ScanIssue from a group of ASCA scan details that are on the same line. + * + * @param ascaScanDetails the list of ASCA scan details for the same line (already sorted by severity) + * @return a ScanIssue representing the ASCA finding(s), or null if conversion fails + */ + private ScanIssue createScanIssueForGroup(List ascaScanDetails) { + if (ascaScanDetails == null || ascaScanDetails.isEmpty()) { + return null; + } + + try { + ScanIssue scanIssue = getScanIssue(ascaScanDetails); + + // Add vulnerabilities from all details in the group + for (int i = 0; i < ascaScanDetails.size(); i++) { + ScanDetail detail = ascaScanDetails.get(i); + String vulnerabilityId = (i == 0) ? scanIssue.getScanIssueId() : null; + Vulnerability vuln = createVulnerability(detail, vulnerabilityId); + scanIssue.getVulnerabilities().add(vuln); + } + + // Update title based on actual number of vulnerabilities + updateScanIssueTitleAndLocation(scanIssue, ascaScanDetails); + + CxLogger.info(LOG_TAG + " Created ScanIssue with " + scanIssue.getVulnerabilities().size() + + " vulnerabilities on line " + scanIssue.getProblematicLineNumber()); + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert scan details group to ScanIssue: " + e.getMessage()); + return null; + } + } + + /** + * Creates a ScanIssue with appropriate title and basic properties from a group of ASCA scan details. + * + * @param ascaScanDetails the list of ASCA scan details (already sorted by severity) + * @return a ScanIssue with basic properties set + */ + private ScanIssue getScanIssue(List ascaScanDetails) { + ScanIssue scanIssue = new ScanIssue(); + ScanDetail firstDetail = ascaScanDetails.get(0); // Highest severity (already sorted) + + // Set title based on whether there are multiple issues on the same line + String title; + if (ascaScanDetails.size() > 1) { + title = ascaScanDetails.size() + MULTIPLE_ISSUES_SUFFIX; + } else { + title = firstDetail.getRuleName(); + } + + scanIssue.setTitle(title); + scanIssue.setDescription(firstDetail.getDescription()); + scanIssue.setSeverity(mapSeverity(firstDetail.getSeverity())); + scanIssue.setFilePath(filePath); + scanIssue.setScanEngine(ScanEngine.ASCA); + scanIssue.setProblematicLineNumber(firstDetail.getLine()); + scanIssue.setRuleId(firstDetail.getRuleID()); + + // Generate unique ID based on line, rule ID, and rule name + String scanIssueId = generateUniqueId(firstDetail); + scanIssue.setScanIssueId(scanIssueId); + + return scanIssue; + } + + /** + * Creates a Vulnerability object from a ASCA scan detail. + * + * @param scanDetail the ASCA scan detail + * @param overrideId optional vulnerability ID to use instead of generating one + * @return a Vulnerability object + */ + private Vulnerability createVulnerability(ScanDetail scanDetail, String overrideId) { + Vulnerability vulnerability = new Vulnerability(); + + // Generate or use provided vulnerability ID + String vulnerabilityId = generateUniqueId(scanDetail); + if (overrideId != null && !overrideId.isBlank()) { + vulnerabilityId = overrideId; + } + + vulnerability.setVulnerabilityId(vulnerabilityId); + vulnerability.setTitle(scanDetail.getRuleName()); + vulnerability.setDescription(scanDetail.getDescription()); + vulnerability.setSeverity(mapSeverity(scanDetail.getSeverity())); + + CxLogger.info(LOG_TAG + " Created vulnerability '" + scanDetail.getRuleName() + + "' with vulnerabilityId '" + vulnerabilityId + "'"); + + return vulnerability; + } + + /** + * Updates the ScanIssue title and location based on vulnerability count and scan details. + */ + private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List ascaScanDetails) { + // Update title based on actual number of vulnerabilities + if (scanIssue.getVulnerabilities().size() == 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().get(0).getTitle()); + } else if (scanIssue.getVulnerabilities().size() > 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + MULTIPLE_ISSUES_SUFFIX); + } + + // Add location information from first detail + ScanDetail firstDetail = ascaScanDetails.get(0); + Location location = new Location(); + location.setLine(firstDetail.getLine()); + scanIssue.getLocations().add(location); + } + + /** + * Maps ASCA severity levels to standardized severity strings. + * + * @param ascaSeverity the ASCA severity level + * @return standardized severity string + */ + private String mapSeverity(String ascaSeverity) { + if (ascaSeverity == null) { + return "Medium"; + } + + switch (ascaSeverity.toLowerCase()) { + case "critical": + return "Critical"; + case "high": + return "High"; + case "medium": + return "Medium"; + case "low": + return "Low"; + case "info": + return "Low"; + default: + return "Medium"; + } + } + + /** + * Get severity precedence for sorting (higher number = higher severity). + */ + private int getSeverityPrecedence(String severity) { + if (severity == null) { + return 3; + } + + switch (severity.toLowerCase()) { + case "critical": + return 5; + case "high": + return 4; + case "medium": + return 3; + case "low": + return 2; + case "info": + return 1; + default: + return 3; + } + } + + /** + * Generates a unique ID for the given scan detail. + */ + private String generateUniqueId(ScanDetail scanDetail) { + if (scanDetail != null) { + return DevAssistUtils.generateUniqueId( + scanDetail.getLine(), + scanDetail.getRuleID() + scanDetail.getRuleName(), + scanDetail.getFileName()); + } + return ScanEngine.ASCA.name(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java new file mode 100644 index 00000000..0bdee66b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java @@ -0,0 +1,39 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * Command for coordinating ASCA scanner operations. + */ +public class AscaScannerCommand { + + private final IProject project; + private final AscaScannerService scannerService; + private static final String LOG_TAG = "[ASCA-COMMAND]"; + + public AscaScannerCommand(IProject project) { + this.project = project; + this.scannerService = new AscaScannerService(project); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + public boolean shouldScan(String filePath) { + return scannerService.shouldScanFile(filePath); + } + + public ScanResult scan(String filePath, IDocument document) { + return scannerService.scan(filePath, document, project); + } + + 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()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java new file mode 100644 index 00000000..6c0d7963 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -0,0 +1,364 @@ +package com.checkmarx.eclipse.devassist.scanners.asca; + +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * 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 { + + private final IProject project; + private static final String LOG_TAG = "[ASCA-SERVICE]"; + private static final String ASCA_DIR = "CxASCA"; + private static final Object SCAN_LOCK = new Object(); + + // Supported extensions for ASCA scanning (based on VSCode/JetBrains + // implementation) + private static final String[] SUPPORTED_EXTENSIONS = { "java", "py", "js", "jsx", "ts", "tsx", "go", "rb", "cs", + "cpp" }; + + public AscaScannerService(IProject project) { + this.project = project; + } + + private String getScannerName() { + return "ASCA"; + } + + private String getLogTag() { + return LOG_TAG; + } + + /** + * Check if file has a supported extension for ASCA scanning. + */ + private boolean isFileTypeSupported(String filePath) { + if (filePath == null) { + return false; + } + + String lowerPath = filePath.toLowerCase(); + for (String ext : SUPPORTED_EXTENSIONS) { + if (lowerPath.endsWith("." + ext)) { + return true; + } + } + return false; + } + + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + String normalized = filePath.replace("\\", "/"); + return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); + } + + public void close() throws Exception { + // No resources to close + } + + /** + * Primary scan method - gets file content and executes scan. + */ + public ScanResult scan(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(getLogTag() + " Could not read file content: " + filePath); + return null; + } + // Run ASCA scan with proper temp file management + Object rawResults = runAscaScan(filePath, fileContent); + if (rawResults == null) { + return null; + } + return new AscaScanResultAdaptor((com.checkmarx.ast.asca.ScanResult) rawResults, filePath); + } catch (Exception e) { + CxLogger.error(getLogTag() + " Scan failed: " + e.getMessage(), e); + return null; + } + } + + /** + * Get file content from document (if available) or from file system. + */ + + private String getFileContent(String filePath, IDocument document) { + // 1. Try reading from the in-memory document buffer first + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + if (filePath == null || filePath.isBlank()) { + return null; + } + // 2. Try resolving filesystem location via Workspace without taking workspace + // locks + java.nio.file.Path nioPath = null; + try { + org.eclipse.core.runtime.IPath eclipsePath = new org.eclipse.core.runtime.Path(filePath); + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(eclipsePath); + + if (file != null && file.getLocation() != null) { + // Get direct OS filesystem path from IFile (prevents blocking + // file.getContents() lock) + nioPath = file.getLocation().toFile().toPath(); + } + } catch (Exception e) { + // Fallback if path isn't a valid workspace path + } + if (nioPath == null) { + try { + nioPath = java.nio.file.Paths.get(filePath); + } catch (Exception e) { + return null; + } + } + // 3. Perform standard Java NIO read on physical path (Interrupt-safe) + try { + if (java.nio.file.Files.exists(nioPath) && java.nio.file.Files.isRegularFile(nioPath)) { + return java.nio.file.Files.readString(nioPath, java.nio.charset.StandardCharsets.UTF_8); + } + } catch (java.io.IOException e) { + CxLogger.warning(getLogTag() + " 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(getLogTag() + " File reading interrupted for: " + filePath); + } else { + CxLogger.warning(getLogTag() + " 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(getLogTag() + " Failed to create temporary file"); + return null; + } + + try { + CxLogger.info(getLogTag() + " Starting ASCA scan: " + filePath); + String ignoreFilePath = getIgnoreFilePath(); + Object scanResult = executeAscaScanner(tempFilePath, ignoreFilePath); + CxLogger.info(getLogTag() + " 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(getLogTag() + " ASCA scan error: " + e.getMessage(), e); + return null; + } + } + + /** + * Get ignore file path for ASCA scanning. + * Returns empty string by default - can be extended to read from .checkmarxIgnored file. + */ + private String getIgnoreFilePath() { + return ""; + } + + /** + * Get secure temporary directory with validation. Prevents directory traversal + * attacks. + */ + private Path getSecureTempDirectory() throws SecurityException { + try { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.trim().isEmpty()) { + throw new SecurityException("System temp directory not available"); + } + + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + + if (!Files.exists(baseTempDir) || !Files.isDirectory(baseTempDir)) { + throw new SecurityException("System temp directory not valid: " + baseTempDir); + } + + Path ascaTempDir = baseTempDir.resolve(ASCA_DIR).normalize(); + + // Security check: ensure ASCA dir is within system temp + if (!ascaTempDir.startsWith(baseTempDir)) { + throw new SecurityException("ASCA temp directory outside system temp"); + } + + return ascaTempDir; + + } catch (Exception e) { + throw new SecurityException("Failed to create secure temp directory", e); + } + } + + private String saveTempFile(String fileName, String fileContent) { + try { + // Get secure temp directory + Path tempDir = getSecureTempDirectory(); + createTempFolder(tempDir); + + // Sanitize fileName to prevent directory traversal attacks + String sanitizedFileName = sanitizeFileName(fileName); + + // Create secure path with normalization + Path tempFilePath = tempDir.resolve(sanitizedFileName).normalize(); + + // Security check: ensure the resolved path is still within the temp directory + if (!tempFilePath.startsWith(tempDir)) { + return null; + } + + Files.write(tempFilePath, fileContent.getBytes()); + return tempFilePath.toAbsolutePath().toString(); + } catch (SecurityException e) { + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Create temp folder if it doesn't exist. + */ + private void createTempFolder(Path tempDir) throws IOException { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } + + /** + * 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(getLogTag() + " Security violation: file outside temp: " + filePath); + return; + } + + Files.deleteIfExists(path); + CxLogger.info(getLogTag() + " Temporary file deleted: " + path); + + } catch (SecurityException e) { + CxLogger.error(getLogTag() + " Security error deleting file: " + e.getMessage(), e); + } catch (IOException e) { + CxLogger.warning(getLogTag() + " Failed to delete temp file: " + filePath); + } catch (Exception e) { + CxLogger.warning(getLogTag() + " Unexpected error deleting temp file: " + e.getMessage()); + } + } + + /** + * Compatibility method matching basescanner.ScannerService interface. + */ + public List scan(String filePath) throws Exception { + if (!shouldScanFile(filePath)) { + return List.of(); + } + var result = scan(filePath, new Document(), project); + return result != null ? result.getIssues() : List.of(); + } + + private com.checkmarx.ast.asca.ScanResult scanAscaFile(String path, boolean ascaLatestVersion, String agent, + String ignoreFilePath) throws IOException, CxException, InterruptedException { + com.checkmarx.ast.asca.ScanResult scanResult = null; + try { + scanResult = CxWrapperFactory.build().ScanAsca(path, ascaLatestVersion, agent, null); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (CxException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + return scanResult; + } + +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java new file mode 100644 index 00000000..f085373d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java @@ -0,0 +1,175 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeImage; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeVulnerability; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adaptor for Container image scan results in Eclipse. + * + * Converts typed container vulnerability data (ContainersRealtimeResults) into + * standardized ScanIssue, Vulnerability, and Location objects. + */ +public class ContainerScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[CONTAINER-ADAPTOR]"; + + private static final String MALICIOUS_RISK_CONTAINER = "Container image contains malicious risk dependencies or configuration."; + private static final String CRITICAL_RISK_CONTAINER = "Container image contains critical severity security vulnerabilities."; + private static final String HIGH_RISK_CONTAINER = "Container image contains high severity security vulnerabilities."; + private static final String MEDIUM_RISK_CONTAINER = "Container image contains medium severity security vulnerabilities."; + private static final String LOW_RISK_CONTAINER = "Container image contains low severity security vulnerabilities."; + + private final ContainersRealtimeResults containersRealtimeResults; + private final String fileType; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of ContainerScanResultAdaptor with typed Container real-time results. + * + * @param containersRealtimeResults the container real-time scan results from AST SDK + * @param fileType the file extension/type (e.g., "dockerfile") + * @param filePath the project-relative or absolute file path + */ + public ContainerScanResultAdaptor(ContainersRealtimeResults containersRealtimeResults, String fileType, String filePath) { + this.containersRealtimeResults = containersRealtimeResults; + this.fileType = fileType; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public ContainersRealtimeResults getResults() { + return containersRealtimeResults; + } + + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Processes images obtained from the scan results and converts them into standardized scan issues. + */ + public List buildIssues() { + List images = Objects.nonNull(getResults()) ? getResults().getImages() : null; + if (Objects.isNull(images) || images.isEmpty()) { + return Collections.emptyList(); + } + return images.stream() + .map(this::createScanIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Creates a ScanIssue object based on the provided ContainersRealtimeImage. + */ + private ScanIssue createScanIssue(ContainersRealtimeImage containersImageObj) { + try { + ScanIssue scanIssue = new ScanIssue(); + scanIssue.setScanEngine(ScanEngine.CONTAINERS); + scanIssue.setTitle(containersImageObj.getImageName()); + scanIssue.setImageTag(containersImageObj.getImageTag()); + scanIssue.setSeverity(DevAssistUtils.normalizeSeverity(containersImageObj.getStatus())); + scanIssue.setFileType(this.fileType); + scanIssue.setFilePath(this.filePath); + + if (Objects.nonNull(containersImageObj.getLocations()) && !containersImageObj.getLocations().isEmpty()) { + containersImageObj.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + if (Objects.nonNull(containersImageObj.getVulnerabilities()) && !containersImageObj.getVulnerabilities().isEmpty()) { + containersImageObj.getVulnerabilities().forEach(vulnerability -> + scanIssue.getVulnerabilities().add(createVulnerability(vulnerability))); + } + + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + + int line = !scanIssue.getLocations().isEmpty() ? scanIssue.getLocations().get(0).getLine() : 1; + scanIssue.setProblematicLineNumber(line); + + return scanIssue; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error creating scan issue for image " + containersImageObj.getImageName() + ": " + e.getMessage()); + return null; + } + } + + /** + * Creates a Vulnerability instance based on the provided ContainersRealtimeVulnerability. + */ + private Vulnerability createVulnerability(ContainersRealtimeVulnerability vulnerabilityObj) { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setCve(vulnerabilityObj.getCve()); + vulnerability.setDescription(this.getDescription(vulnerabilityObj.getSeverity())); + vulnerability.setSeverity(DevAssistUtils.normalizeSeverity(vulnerabilityObj.getSeverity())); + return vulnerability; + } + + /** + * Maps severity string into standard risk description text for container vulnerabilities. + */ + private String getDescription(String severity) { + if (Objects.isNull(severity) || severity.isEmpty()) { + return severity; + } + String normalized = severity.toUpperCase(); + switch (normalized) { + case "MALICIOUS": + return MALICIOUS_RISK_CONTAINER; + case "CRITICAL": + return CRITICAL_RISK_CONTAINER; + case "HIGH": + return HIGH_RISK_CONTAINER; + case "MEDIUM": + return MEDIUM_RISK_CONTAINER; + case "LOW": + return LOW_RISK_CONTAINER; + default: + return severity; + } + } + + /** + * Creates a Location object based on the provided RealtimeLocation. + * Note: Adjusts zero-based line numbers from scan results to one-based line numbers. + */ + private Location createLocation(RealtimeLocation location) { + int line = getLine(location); + int startIndex = location.getStartIndex(); + int endIndex = location.getEndIndex(); + return new Location(line, startIndex, endIndex); + } + + /** + * Retrieves the line number from the given RealtimeLocation object and increments it by 1. + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue. + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() : 0; + return DevAssistUtils.generateUniqueId(line, scanIssue.getTitle(), scanIssue.getImageTag()); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java new file mode 100644 index 00000000..7a8cf70c --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java @@ -0,0 +1,105 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import java.util.Objects; + +/** + * Container Scanner Command that manages the lifecycle of container realtime scanning in Eclipse. + * Coordinates execution, file eligibility validation, and disposal for a given workspace project. + */ +public class ContainerScannerCommand { + + private static final String LOG_TAG = "[CONTAINER-COMMAND]"; + + private final IProject project; + 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) { + this.project = project; + this.containerScannerService = containerScannerService; + initializeScanner(); + } + + /** + * Initializes the scanner, invoked during or after registration of the command. + */ + public synchronized void initializeScanner() { + if (!isInitialized) { + this.isInitialized = true; + String projectName = Objects.nonNull(project) ? project.getName() : "Unknown"; + CxLogger.info(LOG_TAG + " Container Scanner Command initialized for project: " + projectName); + } + } + + /** + * Evaluates whether the specified file path is eligible for a Container scan + * (Dockerfiles, Docker Compose, or Helm charts). + * + * @param filePath project-relative or absolute file path + * @return true if the file should be scanned, false otherwise + */ + public boolean shouldScan(String filePath) { + return containerScannerService.shouldScanFile(filePath); + } + + /** + * Triggers a Container Realtime scan for the specified file path and active document. + * + * @param filePath absolute path to the file being scanned + * @param document the open Eclipse IDocument buffer (or null if scanning directly from disk) + * @return strongly typed ScanResult containing ContainersRealtimeResults and converted ScanIssues + */ + public ScanResult scan(String filePath, IDocument document) { + if (!shouldScan(filePath)) { + return null; + } + return containerScannerService.scan(filePath, document, project); + } + + /** + * Returns the underlying ContainerScannerService instance. + * + * @return the active ContainerScannerService + */ + public ContainerScannerService getScannerService() { + return containerScannerService; + } + + /** + * Disposes underlying resources and cleans up temporary structures. + * Automatically called when the project or plugin context is closed/unloaded. + */ + 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()); + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java new file mode 100644 index 00000000..0c2bc906 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -0,0 +1,288 @@ +package com.checkmarx.eclipse.devassist.scanners.containers; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Container image scanner service for Eclipse. + * + * Handles file detection (Docker, Docker Compose, Helm), secure temporary folder management, + * and direct invocation of Checkmarx Container Realtime scanning via CxWrapperFactory. + */ +public class ContainerScannerService { + + private static final String LOG_TAG = "[CONTAINER-SERVICE]"; + private static final String CONTAINER_DIR = "CxContainer"; + private static final Object SCAN_LOCK = new Object(); + + private static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile*", + "**/*.containerfile", + "**/*.image", + "**/docker-compose*.yml", + "**/docker-compose*.yaml" + ); + + private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of( + "chart.yaml", + "chart.yml", + "values.yaml", + "values.yml" + ); + + private final IProject project; + private String fileType; + + public ContainerScannerService(IProject project) { + this.project = project; + } + + /** + * Determines whether a file path or file context should be scanned by evaluating + * container path patterns and Helm configurations. + * + * @param filePath path to evaluate + * @return {@code true} if eligible for scanning; {@code false} otherwise + */ + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isBlank()) { + return false; + } + + String normalized = filePath.replace("\\", "/"); + if (normalized.contains("/node_modules/")) { + return false; + } + + return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); + } + + /** + * Checks whether the supplied file path matches container file patterns (Dockerfile, Docker Compose, etc.). + */ + private boolean isContainersFilePatternMatching(String filePath) { + String lowerPath = filePath.toLowerCase(); + List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { + if (DevAssistUtils.isDockerComposeFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKER_COMPOSE; + } else if (DevAssistUtils.isDockerFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKERFILE; + } + return true; + } + } + return false; + } + + /** + * Checks whether the supplied file path is part of a Helm chart. + */ + public boolean isHelmFile(String filePath) { + if (filePath == null) { + return false; + } + String lowerPath = filePath.toLowerCase(); + if (DevAssistUtils.isYamlFile(lowerPath)) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { + return false; + } + if (lowerPath.contains("/helm/")) { + this.fileType = DevAssistUtils.HELM; + return true; + } + } + return false; + } + + /** + * Primary scan method. Reads content, creates isolated temporary directory structure, + * executes the container realtime scan, and updates ignored issues. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + synchronized (SCAN_LOCK) { + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); + return null; + } + + Path tempBaseDir = getSecureTempDirectory(); + Path tempSubFolder = null; + Path tempFilePath = null; + + try { + String fileName = Paths.get(filePath).getFileName().toString(); + String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; + String folderName = prefix + generateFileHash(filePath); + + tempSubFolder = tempBaseDir.resolve(folderName).normalize(); + createTempFolder(tempSubFolder); + + tempFilePath = tempSubFolder.resolve(fileName).normalize(); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + + CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); +// String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj != null ? proj : this.project); + + ContainersRealtimeResults scanResults = null; + try { + scanResults = CxWrapperFactory.build().containersRealtimeScan(tempFilePath.toString(), ""); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, filePath); + + return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); + + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); + } finally { + if (Objects.nonNull(tempSubFolder)) { + deleteTempFolder(tempSubFolder); + } + } + } + return null; + } + + /** + * Re-runs scan without ignore settings to calculate line updates for ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// IgnoreManager ignoreManager = new IgnoreManager(proj); +// if (ignoreManager.hasIgnoredEntries(ScanEngine.CONTAINERS)) { +// CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); +// ContainersRealtimeResults fullScanResults = CxWrapperFactory.build() +// .containersRealtimeScan(tempFilePath, ""); +// +// if (fullScanResults != null) { +// ContainerScanResultAdaptor fullScanResultAdaptor = new ContainerScanResultAdaptor(fullScanResults, this.fileType, filePath); +// ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); +// } +// } +// } catch (Exception e) { +// CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored file line numbers: " + e.getMessage()); +// } + } + + /** + * Reads file content from Eclipse IDocument buffer or disk filesystem. + */ + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + /** + * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary directory names. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.isBlank()) { + tempOSPath = System.getProperty("user.home"); + } + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + return baseTempDir.resolve(CONTAINER_DIR).normalize(); + } + + private void createTempFolder(Path tempDir) throws IOException { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } + + private 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 base scanner interfaces. + */ + public List scan(String filePath) throws Exception { + if (!shouldScanFile(filePath)) { + return List.of(); + } + ScanResult result = scan(filePath, null, this.project); + return result != null ? result.getIssues() : List.of(); + } + + public void close() throws Exception { + // No persistent connections to close + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java new file mode 100644 index 00000000..9360815f --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -0,0 +1,254 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Adapter class for handling IaC scan results and converting them into a standardized format. + * + * This class wraps an IaC {@link IacRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on IaC misconfigurations detected in the files. + * + * Features: + * - Groups multiple misconfigurations on the same line + * - Sorts misconfigurations by severity precedence + * - Generates proper unique IDs + * - Tracks location information + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class IacScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[IAC-ADAPTOR]"; + private static final String MULTIPLE_ISSUES_SUFFIX = " IaC misconfigurations"; + + private final IacRealtimeResults iacRealtimeResults; + private final String filePath; + private final List scanIssues; + + public IacScanResultAdaptor(IacRealtimeResults iacRealtimeResults, String filePath) { + this.iacRealtimeResults = iacRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + @Override + public IacRealtimeResults getResults() { + return iacRealtimeResults; + } + + @Override + public List getIssues() { + return scanIssues; + } + + private List buildIssues() { + if (iacRealtimeResults == null || iacRealtimeResults.getResults() == null) { + CxLogger.info(LOG_TAG + " No scan results available"); + return Collections.emptyList(); + } + + List issues = iacRealtimeResults.getResults(); + if (issues.isEmpty()) { + return Collections.emptyList(); + } + + // Group issues by line number, then sort by severity precedence + Map> groupedIssues = issues.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy( + issue -> { + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + return issue.getLocations().get(0).getLine(); + } + return 1; + }, + Collectors.collectingAndThen(Collectors.toList(), issuesList -> { + issuesList.sort(Comparator.comparingInt(issue -> + getSeverityPrecedence(issue.getSeverity()))); + return issuesList; + }) + )); + + List scanIssues = groupedIssues.values().stream() + .map(this::createScanIssueForGroup) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + scanIssues.size() + " grouped scan issues for file: " + filePath); + return scanIssues; + } + + private ScanIssue createScanIssueForGroup(List iacIssues) { + if (iacIssues == null || iacIssues.isEmpty()) { + return null; + } + + try { + ScanIssue scanIssue = getScanIssue(iacIssues); + + // Add vulnerabilities from all issues in the group + for (int i = 0; i < iacIssues.size(); i++) { + IacRealtimeResults.Issue iacIssue = iacIssues.get(i); + String vulnerabilityId = (i == 0) ? scanIssue.getScanIssueId() : null; + Vulnerability vuln = createVulnerability(iacIssue, vulnerabilityId); + scanIssue.getVulnerabilities().add(vuln); + } + + // Update title based on actual number of vulnerabilities + updateScanIssueTitleAndLocation(scanIssue, iacIssues); + + CxLogger.info(LOG_TAG + " Created ScanIssue with " + scanIssue.getVulnerabilities().size() + + " vulnerabilities on line " + scanIssue.getProblematicLineNumber()); + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert issues group to ScanIssue: " + e.getMessage()); + return null; + } + } + + private ScanIssue getScanIssue(List iacIssues) { + ScanIssue scanIssue = new ScanIssue(); + IacRealtimeResults.Issue firstIssue = iacIssues.get(0); + + int firstLine = 1; + if (firstIssue.getLocations() != null && !firstIssue.getLocations().isEmpty()) { + firstLine = firstIssue.getLocations().get(0).getLine(); + } + + // Set title based on whether there are multiple issues on the same line + String title; + if (iacIssues.size() > 1) { + title = iacIssues.size() + MULTIPLE_ISSUES_SUFFIX; + } else { + title = firstIssue.getTitle(); + } + + scanIssue.setTitle(title); + scanIssue.setDescription(firstIssue.getDescription()); + scanIssue.setSeverity(mapSeverity(firstIssue.getSeverity())); + scanIssue.setFilePath(filePath); + scanIssue.setScanEngine(ScanEngine.IAC); + scanIssue.setProblematicLineNumber(firstLine); + + String scanIssueId = generateUniqueId(firstIssue, firstLine); + scanIssue.setScanIssueId(scanIssueId); + + return scanIssue; + } + + private Vulnerability createVulnerability(IacRealtimeResults.Issue iacIssue, String overrideId) { + Vulnerability vulnerability = new Vulnerability(); + + int firstLine = 1; + if (iacIssue.getLocations() != null && !iacIssue.getLocations().isEmpty()) { + firstLine = iacIssue.getLocations().get(0).getLine(); + } + + String vulnerabilityId = generateUniqueId(iacIssue, firstLine); + if (overrideId != null && !overrideId.isBlank()) { + vulnerabilityId = overrideId; + } + + vulnerability.setVulnerabilityId(vulnerabilityId); + vulnerability.setTitle(iacIssue.getTitle()); + vulnerability.setDescription(iacIssue.getDescription()); + vulnerability.setSeverity(mapSeverity(iacIssue.getSeverity())); + + CxLogger.info(LOG_TAG + " Created vulnerability '" + iacIssue.getTitle() + + "' with vulnerabilityId '" + vulnerabilityId + "'"); + + return vulnerability; + } + + private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List iacIssues) { + // Update title based on actual number of vulnerabilities + if (scanIssue.getVulnerabilities().size() == 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().get(0).getTitle()); + } else if (scanIssue.getVulnerabilities().size() > 1) { + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + MULTIPLE_ISSUES_SUFFIX); + } + + // Add location information from issues + for (IacRealtimeResults.Issue iacIssue : iacIssues) { + if (iacIssue.getLocations() != null) { + for (RealtimeLocation loc : iacIssue.getLocations()) { + Location location = new Location(); + location.setLine(loc.getLine()+1); + location.setStartIndex(loc.getStartIndex()); + location.setEndIndex(loc.getEndIndex()); + scanIssue.getLocations().add(location); + } + } + } + + // Ensure at least one location + if (scanIssue.getLocations().isEmpty()) { + Location location = new Location(); + location.setLine(scanIssue.getProblematicLineNumber()); + scanIssue.getLocations().add(location); + } + } + + private String mapSeverity(String severity) { + if (severity == null) { + return "Medium"; + } + + switch (severity.toLowerCase()) { + case "critical": + return "Critical"; + case "high": + return "High"; + case "medium": + return "Medium"; + case "low": + return "Low"; + case "info": + return "Low"; + default: + return "Medium"; + } + } + + private int getSeverityPrecedence(String severity) { + if (severity == null) { + return 3; + } + + switch (severity.toLowerCase()) { + case "critical": + return 5; + case "high": + return 4; + case "medium": + return 3; + case "low": + return 2; + case "info": + return 1; + default: + return 3; + } + } + + private String generateUniqueId(IacRealtimeResults.Issue iacIssue, int line) { + if (iacIssue != null) { + return DevAssistUtils.generateUniqueId( + line, + iacIssue.getSimilarityId() + iacIssue.getTitle(), + filePath); + } + return ScanEngine.IAC.name(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java new file mode 100644 index 00000000..4174cea3 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java @@ -0,0 +1,73 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * Command for coordinating IaC scanner operations. + * + * Manages the lifecycle of IaC realtime scanning in Eclipse, integrating with + * the scanner registry system to handle enabling/disabling of IaC scanning. + */ +public class IacScannerCommand { + + private static final String LOG_TAG = "[IAC-COMMAND]"; + + private final IProject project; + private final IacScannerService scannerService; + + public IacScannerCommand(IProject project, IacScannerService scannerService) { + this.project = project; + this.scannerService = scannerService; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + initializeScanner(); + } + + public IacScannerCommand(IProject project) { + this(project, new IacScannerService(project)); + } + + /** + * Initializes the scanner, invoked after creation / registration of the scanner. + */ + public void initializeScanner() { + // Intentionally empty - mirrors JetBrains implementation where IaC scans + // are triggered on demand via editor file changes rather than bulk project scans. + } + + /** + * Determines whether a file path should be scanned by the IaC scanner. + * + * @param filePath path to evaluate + * @return {@code true} if the file is an IaC file eligible for scanning + */ + public boolean shouldScan(String filePath) { + return scannerService.shouldScanFile(filePath); + } + + /** + * Executes an IaC scan on a specific file given its document content. + * + * @param filePath path to the file being scanned + * @param document editor document content + * @return ScanResult containing issues found, or null + */ + public ScanResult scan(String filePath, IDocument document) { + return scannerService.scan(filePath, document, project); + } + + /** + * Disposes the scanner and releases associated resources. + * Triggered when project is closed or scanner is unregistered. + */ + 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()); + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java new file mode 100644 index 00000000..920f98b6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -0,0 +1,313 @@ +package com.checkmarx.eclipse.devassist.scanners.iac; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime IaC scanner service for Eclipse. + * + * Manages temporary folder creation, file hash generation, type extraction + * (Terraform, CloudFormation, Kubernetes, Dockerfile, etc.), execution of + * Checkmarx IaC real-time scans, and updating ignored issue tracking data. + */ +public class IacScannerService { + + private static final String LOG_TAG = "[IAC-SERVICE]"; + private static final String IAC_DIR = "CxIaC"; + private static final String DOCKERFILE = "dockerfile"; + private static final Object SCAN_LOCK = new Object(); + + // Supported glob patterns for IaC files + private static final List IAC_SUPPORTED_PATTERNS = List.of( + "*.tf", "*.tf.json", + "*.yaml", "*.yml", + "*.json", + "Dockerfile", "Dockerfile.*", "*.dockerfile", "dockerfile", "dockerfile.*" + ); + + // Supported extensions for IaC files + private static final Set IAC_FILE_EXTENSIONS = Set.of( + "tf", "tf.json", "yaml", "yml", "json", "dockerfile" + ); + + private final IProject project; + private String fileType; + + public IacScannerService(IProject project) { + this.project = project; + } + + public String getScannerName() { + return "IAC"; + } + + /** + * 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). + */ + public boolean isFileTypeSupported(String filePath) { + if (filePath == null || filePath.isBlank()) { + return false; + } + + String lowerPath = filePath.toLowerCase(); + List pathMatchers = IAC_SUPPORTED_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path.getFileName())) { + fileType = isDockerFile(lowerPath) ? DOCKERFILE : getFileExtension(filePath); + return true; + } + } + + String extension = getFileExtension(filePath); + if (extension == null) { + return false; + } + + fileType = extension.toLowerCase(); + return IAC_FILE_EXTENSIONS.contains(fileType); + } + + /** + * Determines whether a file should be scanned by evaluating general filters and pattern matching. + */ + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + String normalized = filePath.replace("\\", "/"); + return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); + } + + public void close() throws Exception { + // No resources to release + } + + /** + * Primary scan method. Converts editor/document contents to a temporary isolated file + * and executes the real-time IaC scan via CxWrapperFactory. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + Path tempFolderPath = getSecureTempDirectory(); + Pair saveResult = null; + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempFolderPath); + + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " No content found in file: " + filePath); + return null; + } + + saveResult = saveTempFiles(tempFolderPath, filePath, fileContent); + if (Objects.nonNull(saveResult)) { + String tempFilePath = saveResult.getLeft().toString(); + CxLogger.info(LOG_TAG + " Start IAC Realtime Scan On File: " + filePath); + + String containerTool = "docker"; +// String ignoreFilePath = getIgnoreFilePath(proj); + + IacRealtimeResults scanResults = null; + try { + scanResults = CxWrapperFactory.build() + .iacRealtimeScan(tempFilePath, containerTool, ""); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + if (scanResults == null) { + return null; + } + + IacScanResultAdaptor scanResultAdaptor = new IacScanResultAdaptor(scanResults, filePath); + + // Perform secondary scan to sync updated line numbers for ignored issues if needed +// updateIgnoredFileDataOnLatestResult(tempFilePath, proj, filePath); + + return scanResultAdaptor; + } + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Error executing IaC scanner for " + filePath + ": " + e.getMessage(), e); + } finally { + CxLogger.info(filePath); + if (Objects.nonNull(saveResult)) { + deleteTempFolder(saveResult.getRight()); + } + } + } + return null; + } + + /** + * Compatibility method matching ScannerService interface returning issue lists. + */ + public List scan(String filePath) throws Exception { + if (!shouldScanFile(filePath)) { + return List.of(); + } + ScanResult result = scan(filePath, new Document(), project); + return result != null ? result.getIssues() : List.of(); + } + + /** + * Performs a full scan without passing the ignore file to update line numbers of ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.debug(LOG_TAG + " IaC: Performing full scan without ignore file to update line numbers"); +// +// IacRealtimeResults fullScanResults = CxWrapperFactory.build() +// .iacRealtimeScan(tempFilePath, DevAssistUtils.getContainerTool(), ""); +// +// if (fullScanResults != null) { +// IacScanResultAdaptor fullScanResultAdaptor = new IacScanResultAdaptor(fullScanResults, fileType, filePath); +// // Hook for updating ignored line markers if IgnoreManager is active +// } +// } +// } catch (IOException | CxException | InterruptedException e) { +// CxLogger.warning(LOG_TAG + " RTS-IaC: Exception occurred while performing full scan without ignore file: " + e.getMessage()); +// } + } + + /** + * Saves file content to an isolated subfolder inside the temporary directory using a hashed name. + */ + private Pair saveTempFiles(Path tempFolder, String filePath, String fileContent) throws IOException { + String fileName = Paths.get(filePath).getFileName().toString(); + Path tempSubFolder = tempFolder.resolve(fileName + "-" + generateFileHash(fileName)); + return createSubFolderAndSaveFile(tempSubFolder, fileName, fileContent); + } + + /** + * Creates a target subfolder and writes the file content. + */ + private Pair createSubFolderAndSaveFile(Path tempSubFolder, String fileName, String fileContent) throws IOException { + createTempFolder(tempSubFolder); + Path fullTargetPath = tempSubFolder.resolve(fileName); + Files.writeString(fullTargetPath, fileContent, StandardCharsets.UTF_8); + return Pair.of(fullTargetPath, tempSubFolder); + } + + /** + * Generates a 16-character SHA-256 hash derived from the relative file path and timestamp. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + return Paths.get(tempOSPath, IAC_DIR).toAbsolutePath().normalize(); + } + + private void createTempFolder(Path tempDir) throws IOException { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } + + private void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + private boolean isDockerFile(String filePath) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + return fileName.contains("dockerfile"); + } + + private String getFileExtension(String filePath) { + if (filePath == null) { + return null; + } + int lastDot = filePath.lastIndexOf('.'); + if (lastDot > 0 && lastDot < filePath.length() - 1) { + return filePath.substring(lastDot + 1).toLowerCase(); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java new file mode 100644 index 00000000..1037573e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java @@ -0,0 +1,196 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.ast.ossrealtime.OssRealtimeScanPackage; +import com.checkmarx.ast.ossrealtime.OssRealtimeVulnerability; +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adaptor class for handling OSS scan results and converting them into a standardized format + * using the {@link ScanResult} interface. + * + * This class wraps an {@link OssRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on vulnerabilities detected in the packages. + * + * Adapted from JetBrains implementation for Eclipse platform. + */ +public class OssScanResultAdaptor implements ScanResult { + + private static final String LOG_TAG = "[OSS-ADAPTOR]"; + + private final OssRealtimeResults ossRealtimeResults; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of {@code OssScanResultAdaptor} with the specified OSS real-time results. + * + * @param ossRealtimeResults the OSS real-time scan results to be wrapped by this adapter + * @param filePath the path of the file being scanned + */ + public OssScanResultAdaptor(OssRealtimeResults ossRealtimeResults, String filePath) { + this.ossRealtimeResults = ossRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + /** + * Retrieves the raw OSS real-time scan results wrapped by this adapter. + * + * @return an {@link OssRealtimeResults} instance containing the results of the OSS scan + */ + @Override + public OssRealtimeResults getResults() { + return ossRealtimeResults; + } + + /** + * Retrieves a list of scan issues discovered in the OSS real-time scan. + * + * @return a list of {@link ScanIssue} objects representing findings, or an empty list if none + */ + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Builds a list of ScanIssue objects from the OSS scan results. + * Processes packages obtained from scan results into standardized ScanIssue items. + * + * @return a list of ScanIssue objects + */ + private List buildIssues() { + List packages = Objects.nonNull(getResults()) ? getResults().getPackages() : null; + if (Objects.isNull(packages) || packages.isEmpty()) { + CxLogger.info(LOG_TAG + " No scan results or packages available"); + return Collections.emptyList(); + } + + List issues = packages.stream() + .map(this::createScanIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + CxLogger.info(LOG_TAG + " Converted " + issues.size() + " OSS scan issues for file: " + filePath); + return issues; + } + + /** + * Creates a {@link ScanIssue} object based on the provided {@link OssRealtimeScanPackage}. + * + * @param packageObj the package object containing scan findings + * @return a structured {@link ScanIssue} instance + */ + private ScanIssue createScanIssue(OssRealtimeScanPackage packageObj) { + if (packageObj == null) { + return null; + } + + try { + ScanIssue scanIssue = new ScanIssue(); + + scanIssue.setPackageManager(packageObj.getPackageManager()); + scanIssue.setTitle(packageObj.getPackageName()); + scanIssue.setPackageVersion(packageObj.getPackageVersion()); + scanIssue.setScanEngine(ScanEngine.OSS); + scanIssue.setSeverity(DevAssistUtils.normalizeSeverity(packageObj.getStatus())); + scanIssue.setFilePath(this.filePath); + + // Process location information + if (Objects.nonNull(packageObj.getLocations()) && !packageObj.getLocations().isEmpty()) { + packageObj.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + // Process vulnerabilities + if (Objects.nonNull(packageObj.getVulnerabilities()) && !packageObj.getVulnerabilities().isEmpty()) { + packageObj.getVulnerabilities().forEach(vulnerability -> + scanIssue.getVulnerabilities().add(createVulnerability(vulnerability))); + } + + // Set primary problem line based on first location (if available) + int primaryLine = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() + : 1; + scanIssue.setProblematicLineNumber(primaryLine); + + // Generate unique ID based on line, package manager + title, and version + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + + return scanIssue; + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to convert package to ScanIssue: " + e.getMessage()); + return null; + } + } + + /** + * Creates a {@link Vulnerability} instance based on the provided {@link OssRealtimeVulnerability}. + * + * @param vulnerabilityObj the OSS vulnerability object + * @return a standardized {@link Vulnerability} object + */ + private Vulnerability createVulnerability(OssRealtimeVulnerability vulnerabilityObj) { + Vulnerability vulnerability = new Vulnerability(); + + vulnerability.setCve(vulnerabilityObj.getCve()); + vulnerability.setTitle(vulnerabilityObj.getCve()); + vulnerability.setDescription(vulnerabilityObj.getDescription()); + vulnerability.setSeverity(DevAssistUtils.normalizeSeverity(vulnerabilityObj.getSeverity())); + vulnerability.setFixVersion(vulnerabilityObj.getFixVersion()); + + return vulnerability; + } + + /** + * Creates a {@link Location} object based on the provided {@link RealtimeLocation}. + * + * @param location the real-time location details + * @return a new {@link Location} instance with 1-based line indexing + */ + private Location createLocation(RealtimeLocation location) { + return new Location(getLine(location), location.getStartIndex(), location.getEndIndex()); + } + + /** + * Adjusts zero-based line numbers from OSS scanner to 1-based line numbers. + * + * @param location the real-time location + * @return 1-based line number + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue using line, package identifier, and version. + * + * @param scanIssue the scan issue + * @return unique string identifier + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() + : 0; + + return DevAssistUtils.generateUniqueId( + line, + scanIssue.getPackageManager() + scanIssue.getTitle(), + scanIssue.getPackageVersion() + ); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java new file mode 100644 index 00000000..7afb1148 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -0,0 +1,190 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +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 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; + +/** + * Command for coordinating OSS scanner operations in Eclipse. + * + * Manages the lifecycle and initialization of OSS scanning: + * - Traverses project workspace files recursively upon initialization + * - Executes background job scans on supported manifest files + * - Publishes findings via ProblemHolderService + */ +public class OssScannerCommand { + + private static final String LOG_TAG = "[OSS-COMMAND]"; + + // Manifest pattern list mirroring DevAssistConstants.MANIFEST_FILE_PATTERNS + private static final List MANIFEST_FILE_PATTERNS = List.of( + "package.json", "package-lock.json", "npm-shrinkwrap.json", + "pom.xml", + "go.mod", "go.sum", + "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", + "Gemfile", "Gemfile.lock", + "Cargo.toml", "Cargo.lock", + "composer.json", "composer.lock", + "packages.config", "*.csproj", + "yarn.lock", ".npm" + ); + + public final OssScannerService ossScannerService; + private final IProject project; + + public OssScannerCommand(IProject project, OssScannerService ossScannerService) { + this.ossScannerService = ossScannerService; + this.project = project; + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + initializeScanner(); + } + + public OssScannerCommand(IProject project) { + this(project, new OssScannerService(project)); + } + + /** + * Initializes the scanner, invoked after creation. + * Launches a background Eclipse Job to scan all manifest files in the project workspace. + */ + protected void initializeScanner() { + Job scanJob = new Job("Starting Checkmarx OSS Real-time Scan") { + @Override + protected IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Scanning manifest files in project: " + project.getName(), IProgressMonitor.UNKNOWN); + scanAllManifestFilesInFolder(monitor); + monitor.done(); + return Status.OK_STATUS; + } + }; + scanJob.schedule(); + } + + /** + * Scans all manifest files in the opened project workspace. + * Recursively iterates through project resources (excluding node_modules) + * and triggers an OSS real-time scan on each matching manifest file. + */ + private void scanAllManifestFilesInFolder(IProgressMonitor monitor) { + if (project == null || !project.isOpen()) { + return; + } + + List matchedFiles = new ArrayList<>(); + + List pathMatchers = MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + try { + // Recursively traverse project workspace files (equivalent to ProjectRootManager in JetBrains) + project.accept(new IResourceVisitor() { + @Override + public boolean visit(IResource resource) throws CoreException { + if (monitor.isCanceled()) { + return false; + } + + // Skip node_modules folder subtree entirely + if (resource.getType() == IResource.FOLDER && resource.getName().equals("node_modules")) { + return false; + } + + if (resource.getType() == IResource.FILE && resource.exists()) { + IFile file = (IFile) resource; + String path = file.getLocation() != null ? file.getLocation().toOSString() : file.getFullPath().toString(); + + for (PathMatcher matcher : pathMatchers) { + if (matcher.matches(Paths.get(path))) { + matchedFiles.add(file); + break; + } + } + } + return true; + } + }); + } catch (CoreException e) { + CxLogger.error(LOG_TAG + " Exception during workspace traversal for project " + project.getName() + ": " + e.getMessage(), e); + } + + // Execute scan on each discovered manifest file + for (IFile file : matchedFiles) { + if (monitor.isCanceled()) { + break; + } + + String uri = file.getLocation() != null ? file.getLocation().toOSString() : file.getFullPath().toString(); + try { + // Perform OSS scan using service + ScanResult ossRealtimeResults = ossScannerService.scan(uri, new Document(), project); + + if (Objects.isNull(ossRealtimeResults)) { + CxLogger.warning(LOG_TAG + " Scan failed for manifest file: " + uri); + continue; + } + + // Add findings to problem markers + List issues = ossRealtimeResults.getIssues(); + ProblemHolderService.addToCxOneFindings(file, issues); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Scan failed for manifest file: " + uri + " with exception: " + e.getMessage()); + } + } + } + + /** + * Check if a file should be scanned by this command. + */ + public boolean shouldScan(String filePath) { + return ossScannerService.shouldScanFile(filePath); + } + + /** + * Execute scan on a file with document content. + */ + public ScanResult scan(String filePath, IDocument document) { + return ossScannerService.scan(filePath, document, project); + } + + /** + * Execute scan on a file path directly. + */ + public ScanResult scan(String filePath) { + return ossScannerService.scan(filePath, new Document(), project); + } + + /** + * Disposes the scanner and releases resources. + */ + 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()); + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java new file mode 100644 index 00000000..ab97ec9b --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -0,0 +1,358 @@ +package com.checkmarx.eclipse.devassist.scanners.oss; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime OSS manifest scanner service for Eclipse that handles temporary file isolation, + * companion lock file resolution (e.g. package-lock.json), and invocation of the Checkmarx OSS engine. + * + * Adapted to mirror JetBrains scanner service features. + */ +public class OssScannerService { + + private static final String LOG_TAG = "[OSS-SERVICE]"; + private static final String OSS_DIR = "CxOSS"; + private static final Object SCAN_LOCK = new Object(); + + private static final List MANIFEST_FILE_PATTERNS = List.of( + "**/Directory.Packages.props", + "**/packages.config", + "**/pom.xml", + "**/package.json", + "**/requirements.txt", + "**/go.mod", + "**/*.csproj", + "**/build.gradle", + "**/build.gradle.kts", + "**/yarn.lock", + "**/*.sbt", + "**/Gemfile", + "**/bower.json", + "**/requirement-*.txt", + "**/requirements-*.txt", + "**/Setup.py", + "**/Setup.cfg", + "**/pyproject.toml", + "**/poetry.lock", + "**/Package.swift", + "**/Package.resolved", + "**/composer.json", + "**/composer.lock", + "**/*.podspec.json", + "**/*.podspec", + "**/Podfile", + "**/Podfile.lock", + "**/Cartfile.resolved", + "**/Gemfile.lock", + "**/Gemfile", + "**/cpanfile.snapshot", + "**/cpanfile", + "**/pubspec.lock" + + ); + + private final IProject project; + + public OssScannerService(IProject project) { + this.project = project; + } + + public String getScannerName() { + return "OSS"; + } + + /** + * Checks whether the supplied file path matches any of the manifest glob patterns. + */ + public boolean isFileTypeSupported(String filePath) { + if (filePath == null) { + return false; + } + + Path path = Paths.get(filePath); + List pathMatchers = MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path)) { + return true; + } + } + return false; + } + + /** + * Determines if a given file should be scanned by the OSS scanner. + */ + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + String normalized = filePath.replace("\\", "/"); + return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); + } + + public void close() throws Exception { + // No resources to close + } + + /** + * Primary scan method - gets file content, isolates into temp folder with companion files, and executes scan. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + 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 = getTempSubFolderPath(filePath); + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempSubFolder); + + Optional mainTempPath = saveMainManifestFile(tempSubFolder, filePath, fileContent); + if (mainTempPath.isEmpty()) { + return null; + } + + // Copy companion lock file (e.g., package-lock.json) into temp folder if available + saveCompanionFile(tempSubFolder, filePath); + + CxLogger.info(LOG_TAG + " Starting Realtime OSS Scan on File: " + filePath); +// String ignoreFilePath = getIgnoreFilePath(proj); + + OssRealtimeResults scanResults = CxWrapperFactory.build().ossRealtimeScan(mainTempPath.get(), ""); + if (scanResults == null) { + return null; + } + + OssScanResultAdaptor scanResultAdaptor = new OssScanResultAdaptor(scanResults, filePath); + + // Performs secondary scan if needed to keep line numbers updated for ignored packages +// updateIgnoredFileDataOnLatestResult(mainTempPath.get(), proj, 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); + } + } + } + + /** + * Compatibility method matching ScannerService interface returning issue lists. + */ + public List scan(String filePath) throws Exception { + if (!shouldScanFile(filePath)) { + return List.of(); + } + ScanResult result = scan(filePath, new Document(), project); + return result != null ? result.getIssues() : List.of(); + } + + /** + * Performs full scan without passing ignore file to update line numbers of ignored entries. + */ +// private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +// try { +// // Extension point for ignore manager syncing when ignore files are active +// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); +// OssRealtimeResults fullScanResults = CxWrapperFactory.build().ossRealtimeScan(tempFilePath, ""); +// if (fullScanResults != null && fullScanResults.getPackages() != null) { +// OssScanResultAdaptor fullScanResultAdaptor = new OssScanResultAdaptor(fullScanResults, filePath); +// // Connects with ignore manager line number updater if implemented +// } +// } +// } catch (Exception e) { +// CxLogger.warning(LOG_TAG + " Exception occurred while performing full scan without ignore file: " + e.getMessage()); +// } +// } + + /** + * Persists the main manifest file into the temporary directory for scanning. + */ + private Optional saveMainManifestFile(Path tempSubFolder, String originalFilePath, String fileContent) { + try { + String fileName = Paths.get(originalFilePath).getFileName().toString(); + Path tempFilePath = tempSubFolder.resolve(fileName); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + return Optional.of(tempFilePath.toString()); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to write main manifest temp file: " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * Copies a companion lock file (e.g., package-lock.json) into the temporary directory + * when it exists alongside the scanned manifest. + */ + private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { + if (originalFilePath == null || originalFilePath.isEmpty() || tempFolderPath == null) { + return; + } + + Path originalPath = Paths.get(originalFilePath); + String parentFileName = originalPath.getFileName().toString(); + String companionFileName = getCompanionFileName(parentFileName); + + if (companionFileName.isEmpty()) { + return; + } + + Path parentPath = originalPath.getParent(); + if (parentPath == null) { + return; + } + + Path companionOriginalPath = parentPath.resolve(companionFileName); + if (!Files.exists(companionOriginalPath)) { + return; + } + + Path companionTempPath = tempFolderPath.resolve(companionFileName); + try { + Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); + CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); + } + } + + /** + * Infers companion lock file name based on manifest file name. + */ + private String getCompanionFileName(String fileName) { + if ("package.json".equalsIgnoreCase(fileName)) { + return "package-lock.json"; + } + if (fileName.toLowerCase().endsWith(".csproj")) { + return "package.lock.json"; + } + return ""; + } + + /** + * Resolves temporary sub-folder path allocated for the file scan. + */ + private Path getTempSubFolderPath(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()); + } + } + + private void createTempFolder(Path tempDir) throws IOException { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } + + private void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java new file mode 100644 index 00000000..586058a3 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java @@ -0,0 +1,161 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.ast.realtime.RealtimeLocation; +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Adapter class for handling Secrets scan results and converting them into a standardized format + * using the {@link ScanResult} interface. + * This class wraps a {@link SecretsRealtimeResults} instance and provides methods to process and extract + * meaningful scan issues based on secrets detected in the files. + */ +public class SecretsScanResultAdaptor implements ScanResult { + + private final SecretsRealtimeResults secretsRealtimeResults; + private final String filePath; + private final List scanIssues; + + /** + * Constructs an instance of {@code SecretsScanResultAdaptor} with the specified Secrets real-time results. + * This adapter allows conversion and processing of Secrets scan results into a standardized format. + * + * @param secretsRealtimeResults the Secrets real-time scan results to be wrapped by this adapter + * @param filePath the path of the scanned file + */ + public SecretsScanResultAdaptor(SecretsRealtimeResults secretsRealtimeResults, String filePath) { + this.secretsRealtimeResults = secretsRealtimeResults; + this.filePath = filePath; + this.scanIssues = buildIssues(); + } + + /** + * Retrieves the Secrets real-time scan results wrapped by this adapter. + * + * @return the Secrets scan results instance containing the results of the Secrets scan + */ + @Override + public SecretsRealtimeResults getResults() { + return secretsRealtimeResults; + } + + /** + * Retrieves a list of scan issues discovered in the Secrets real-time scan. + * + * @return a list of {@code ScanIssue} objects representing the secrets found during the scan + */ + @Override + public List getIssues() { + return scanIssues; + } + + /** + * Retrieves a list of scan issues discovered in the Secrets real-time scan. + * This method processes the secrets obtained from the scan results, + * converts them into standardized scan issues, and returns the list. + * If no secrets are found, an empty list is returned. + * + * @return a list of {@code ScanIssue} objects representing the secrets found during the scan, + * or an empty list if no secrets are detected. + */ + public List buildIssues() { + if (Objects.isNull(getResults())) { + return Collections.emptyList(); + } + + List secrets = getResults().getSecrets(); + if (Objects.isNull(secrets) || secrets.isEmpty()) { + return Collections.emptyList(); + } + + return secrets.stream() + .map(this::createScanIssue) + .collect(Collectors.toList()); + } + + /** + * Creates a {@code ScanIssue} object based on the provided secret result. + * The method processes the secret details and converts them into a structured format to + * represent a scan issue. + * + * @param secret the secret result containing information about the detected secret, + * including its title, severity, description, and locations. + * @return a {@code ScanIssue} object encapsulating the details such as title, scan engine, + * severity, and secret locations derived from the provided secret result. + */ + private ScanIssue createScanIssue(SecretsRealtimeResults.Secret secret) { + ScanIssue scanIssue = new ScanIssue(); + + scanIssue.setTitle(secret.getTitle()); + scanIssue.setScanEngine(ScanEngine.SECRETS); + scanIssue.setSeverity(secret.getSeverity()); + scanIssue.setFilePath(this.filePath); + scanIssue.setDescription(secret.getDescription()); // Set description on ScanIssue for tooltip display + scanIssue.setSecretValue(secret.getSecretValue()); + + // Add locations if available + if (Objects.nonNull(secret.getLocations()) && !secret.getLocations().isEmpty()) { + secret.getLocations().forEach(location -> + scanIssue.getLocations().add(createLocation(location))); + } + + // Fallback location if none are provided by the engine + if (scanIssue.getLocations().isEmpty()) { + Location fallbackLocation = new Location(1, 0, 100); + scanIssue.getLocations().add(fallbackLocation); + } + + // Create vulnerability with secret details + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setTitle(secret.getTitle()); + vulnerability.setDescription(secret.getDescription()); + vulnerability.setSeverity(secret.getSeverity()); + + scanIssue.getVulnerabilities().add(vulnerability); + scanIssue.setScanIssueId(getUniqueId(scanIssue)); + return scanIssue; + } + + /** + * Creates a {@code Location} object based on the provided location information. + * This method extracts the line, start index, and end index from the given + * location and constructs a new {@code Location} instance. + * + * @param location the location containing details such as line, + * start index, and end index for the location. + * @return a new {@code Location} instance with the appropriate line and indices. + */ + private Location createLocation(RealtimeLocation location) { + return new Location(getLine(location), location.getStartIndex(), location.getEndIndex()); + } + + /** + * Retrieves the line number from the given {@code RealtimeLocation} object, increments it by one, and returns the result. + * + * @param location the {@code RealtimeLocation} object containing the original line number + * @return the incremented line number based on the {@code RealtimeLocation}'s line value + * @apiNote Current Secrets scan result line numbers are zero-based, so this method adjusts them to be one-based. + */ + private int getLine(RealtimeLocation location) { + return location.getLine() + 1; + } + + /** + * Generates a unique ID for the given scan issue. + */ + private String getUniqueId(ScanIssue scanIssue) { + int line = (Objects.nonNull(scanIssue.getLocations()) && !scanIssue.getLocations().isEmpty()) + ? scanIssue.getLocations().get(0).getLine() : 0; + return DevAssistUtils.generateUniqueId(line, scanIssue.getTitle(), scanIssue.getDescription()); + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java new file mode 100644 index 00000000..6b2b61f2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java @@ -0,0 +1,38 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +/** + * Command for coordinating Secrets scanner operations. + */ +public class SecretsScannerCommand { + + private final IProject project; + private final SecretsScannerService scannerService; + private static final String LOG_TAG = "[SECRETS-COMMAND]"; + + public SecretsScannerCommand(IProject project) { + this.project = project; + this.scannerService = new SecretsScannerService(project); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); + } + + public boolean shouldScan(String filePath) { + return scannerService.shouldScanFile(filePath); + } + + public SecretsScanResultAdaptor scan(String filePath, IDocument document) { + return scannerService.scan(filePath, document, project); + } + + 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()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java new file mode 100644 index 00000000..e64c1336 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -0,0 +1,291 @@ +package com.checkmarx.eclipse.devassist.scanners.secrets; + +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Realtime Secrets scanner service for Eclipse. + * + * Manages temporary directory creation, file hashing, file exclusion filtering, + * execution of Checkmarx Secrets real-time scans via CxWrapperFactory, and updating + * line numbers for ignored secrets. + */ +public class SecretsScannerService { + + private static final String LOG_TAG = "[SECRETS-SERVICE]"; + private static final String SECRETS_DIR = "CxSecrets"; + private static final Object SCAN_LOCK = new Object(); + + // Glob patterns for manifest files that should be excluded from Secrets scanning + private static final List MANIFEST_FILE_PATTERNS = List.of( + "package.json", "pom.xml", "go.mod", "requirements.txt", + "Gemfile", "Cargo.toml", "composer.json", "package-lock.json", "yarn.lock" + ); + + private final IProject project; + + public SecretsScannerService(IProject project) { + this.project = project; + } + + public String getScannerName() { + return "SECRETS"; + } + + /** + * Determines whether a file should be excluded from Secrets scanning. + */ + private boolean isExcludedFileForSecretsScanning(String filePath) { + if (filePath == null || filePath.isBlank()) { + return true; + } + + Path path = Paths.get(filePath.toLowerCase()); + List manifestMatchers = MANIFEST_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + for (PathMatcher matcher : manifestMatchers) { + if (matcher.matches(path.getFileName())) { + return true; + } + } + + // Exclude Checkmarx ignore list files + String normalized = filePath.replace("\\", "/"); + return normalized.contains("/.checkmarxIgnored") || + normalized.contains("/.checkmarxIgnoredTempList"); + } + + /** + * Checks if the given file is eligible for Secrets scanning. + */ + public boolean shouldScanFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + String normalized = filePath.replace("\\", "/"); + if (normalized.contains("/node_modules/")) { + return false; + } + return !isExcludedFileForSecretsScanning(filePath); + } + + 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 SecretsScanResultAdaptor scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + Path tempSubFolder = getTempSubFolderPath(filePath); + + synchronized (SCAN_LOCK) { + try { + createTempFolder(tempSubFolder); + + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(filePath + " Secrets scanner: file content is empty or unreadable"); + return null; + } + + Optional tempFilePath = saveFileForScanning(tempSubFolder, filePath, fileContent); + if (tempFilePath.isEmpty()) { + CxLogger.warning(LOG_TAG + " Secrets scanner: failed to save file - " + filePath); + return null; + } + + CxLogger.info(LOG_TAG + " Starting scan: " + filePath); +// String ignoreFilePath = getIgnoreFilePath(proj); + + SecretsRealtimeResults scanResults = CxWrapperFactory.build() + .secretsRealtimeScan(tempFilePath.get(), ""); + + if (scanResults == null) { + CxLogger.warning(LOG_TAG + " Secrets scanner: no results returned - " + filePath); + return null; + } + + int secretCount = scanResults.getSecrets() != null ? scanResults.getSecrets().size() : 0; + CxLogger.info(LOG_TAG + " Scan completed: " + filePath + " (" + secretCount + " secrets found)"); + + SecretsScanResultAdaptor scanResultAdaptor = new SecretsScanResultAdaptor(scanResults, filePath); + + // Perform secondary scan to update line numbers for ignored entries if required + updateIgnoredFileDataOnLatestResult(tempFilePath.get(), proj, filePath); + + return scanResultAdaptor; + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Secrets scanner error for " + filePath + ": " + e.getMessage(), e); + } finally { + CxLogger.warning(LOG_TAG + " Cleaning up temp folder: " + tempSubFolder); + deleteTempFolder(tempSubFolder); + } + } + return null; + } + + /** + * Compatibility method matching ScannerService interface returning issue lists. + */ + public List scan(String filePath) throws Exception { + if (!shouldScanFile(filePath)) { + return List.of(); + } + ScanResult result = scan(filePath, new Document(), project); + return result != null ? result.getIssues() : List.of(); + } + + /** + * Performs a full scan without passing the ignore file to update line numbers of ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { +//// String ignoreFilePath = getIgnoreFilePath(proj); +// if (ignoreFilePath != null && !ignoreFilePath.isBlank() && new File(ignoreFilePath).exists()) { +// CxLogger.warning(LOG_TAG + " Secrets: Performing full scan without ignore file to update line numbers"); +// +// SecretsRealtimeResults fullScanResults = null; +// try { +// fullScanResults = CxWrapperFactory.build() +// .secretsRealtimeScan(tempFilePath, ""); +// } catch (Exception e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// +// if (fullScanResults != null) { +// SecretsScanResultAdaptor fullScanResultAdaptor = new SecretsScanResultAdaptor(fullScanResults, filePath); +// // Hook for updating ignored line markers if IgnoreManager is active +// } +// } + } + + /** + * Resolves a unique subfolder path for storing the temporary file. + */ + private Path getTempSubFolderPath(String originalFilePath) { + Path baseTempPath = getSecureTempDirectory(); + String safeFileName = toSafeTempFileName(originalFilePath); + return baseTempPath.resolve(safeFileName); + } + + /** + * Creates a deterministic, filesystem-safe file name containing base name and a hash suffix. + */ + private String toSafeTempFileName(String filePath) { + String baseName = Paths.get(filePath).getFileName().toString(); + String hash = generateFileHash(filePath); + return baseName + "-" + hash; + } + + /** + * Generates a 16-character SHA-256 hash derived from the relative file path, timestamp, and UUID. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + /** + * Saves the content into a temporary file inside the target subfolder. + */ + private Optional saveFileForScanning(Path tempSubFolder, String originalFilePath, String fileContent) throws IOException { + String fileName = Paths.get(originalFilePath).getFileName().toString(); + Path tempFilePath = tempSubFolder.resolve(fileName); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + return Optional.of(tempFilePath.toString()); + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + return Paths.get(tempOSPath, SECRETS_DIR).toAbsolutePath().normalize(); + } + + private void createTempFolder(Path tempDir) throws IOException { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + } + + private void deleteTempFolder(Path tempDir) { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (var stream = Files.walk(tempDir)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to clean up temp folder: " + e.getMessage()); + } + } + + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + +// private String getIgnoreFilePath(IProject proj) { +// try { +// return DevAssistUtils.getIgnoreFilePath(proj); +// } catch (Exception e) { +// return ""; +// } +// } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java new file mode 100644 index 00000000..b32d517a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java new file mode 100644 index 00000000..74cbd7ea --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java @@ -0,0 +1,46 @@ +package com.checkmarx.eclipse.devassist.state; + +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the current state of scanner enable/disable settings. + * Holds which scanners are enabled and scan frequency preference. + */ +public class ScannerState { + + private final Map scannerStates = new HashMap<>(); + private ScanFrequency frequency; + + public ScannerState() { + initializeDefaults(); + } + + private void initializeDefaults() { + for (ScanEngine engine : ScanEngine.values()) { + scannerStates.put(engine, true); + } + this.frequency = ScanFrequency.ON_DOCUMENT_CHANGE; + } + + public boolean isEnabled(ScanEngine engine) { + return scannerStates.getOrDefault(engine, true); + } + + public void setEnabled(ScanEngine engine, boolean enabled) { + scannerStates.put(engine, enabled); + } + + public ScanFrequency getFrequency() { + return frequency; + } + + public void setFrequency(ScanFrequency frequency) { + this.frequency = frequency; + } + + public Map getAllStates() { + return new HashMap<>(scannerStates); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java new file mode 100644 index 00000000..f3493161 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java @@ -0,0 +1,73 @@ +package com.checkmarx.eclipse.devassist.state; + +import org.eclipse.jface.preference.IPreferenceStore; + +import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +/** + * Manages scanner state persistence using Eclipse preferences. + * Loads and saves which scanners are enabled/disabled and scan frequency preference. + */ +public class ScannerStateManager { + + private static final String KEY_PREFIX = "scanner."; + private static final String KEY_ENABLED_SUFFIX = ".enabled"; + private static final String KEY_FREQUENCY = "scan.frequency"; + + private final IPreferenceStore prefs; + + public ScannerStateManager() { + this.prefs = Activator.getDefault().getPreferenceStore(); + } + + public ScannerStateManager(IPreferenceStore prefs) { + this.prefs = prefs; + } + + public ScannerState loadState() { + ScannerState state = new ScannerState(); + + for (ScanEngine engine : ScanEngine.values()) { + String key = getEnabledKey(engine); + boolean enabled = prefs.getBoolean(key); + state.setEnabled(engine, enabled); + } + + String freqKey = prefs.getString(KEY_FREQUENCY); + state.setFrequency(ScanFrequency.fromKey(freqKey)); + + return state; + } + + public void saveState(ScannerState state) { + for (ScanEngine engine : ScanEngine.values()) { + String key = getEnabledKey(engine); + boolean enabled = state.isEnabled(engine); + prefs.setValue(key, enabled); + } + + prefs.setValue(KEY_FREQUENCY, state.getFrequency().getKey()); + } + + public boolean isScannerEnabled(ScanEngine engine) { + return prefs.getBoolean(getEnabledKey(engine)); + } + + public void setScannerEnabled(ScanEngine engine, boolean enabled) { + prefs.setValue(getEnabledKey(engine), enabled); + } + + public ScanFrequency getScanFrequency() { + String key = prefs.getString(KEY_FREQUENCY); + return ScanFrequency.fromKey(key); + } + + public void setScanFrequency(ScanFrequency frequency) { + prefs.setValue(KEY_FREQUENCY, frequency.getKey()); + } + + private String getEnabledKey(ScanEngine engine) { + return KEY_PREFIX + engine.name().toLowerCase() + KEY_ENABLED_SUFFIX; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java new file mode 100644 index 00000000..f809bd39 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -0,0 +1,1568 @@ +package com.checkmarx.eclipse.devassist.ui.findings; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.part.ViewPart; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.PreferencesUtil; +import org.eclipse.ui.ide.IDE; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; + +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsContentProvider; +import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsLabelProvider; +import com.checkmarx.eclipse.utils.PluginConstants; +import com.checkmarx.eclipse.utils.PluginUtils; +import com.checkmarx.eclipse.views.CheckmarxView; +import com.checkmarx.eclipse.views.actions.ActionOpenPreferencesPage; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; +import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.ui.findings.actions.VulnerabilityFilterAction; +import com.checkmarx.eclipse.devassist.ui.findings.actions.VulnerabilityFilterState; +import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore; +import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore.IgnoredProblemsListener; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; + + +/** + * Custom Findings View for displaying Checkmarx scan results. + * Extends {@link ViewPart} to provide a custom view in Eclipse. + * Manages a tree view of vulnerabilities with filtering and navigation capabilities. + * Uses {@link TreeViewer} for flexible tree rendering with custom providers. + */ +public class CxFindingsView extends ViewPart implements IgnoredProblemsListener { + + public static final String ID = "com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView"; + private org.osgi.service.event.EventHandler eventHandler; + private org.osgi.service.event.EventHandler settingsEventHandler; + + private TreeViewer treeViewer; + private Map> currentIssues = new HashMap<>(); + private IgnoredProblemsStore ignoredStore; + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + public static final Image FINDINGS_PROMOTIONAL_CUBE = createScaledImage("/icons/cx-one-assist-cube.png", 240); + + public CxFindingsView() { + super(); + } + + + @Override + public void createPartControl(Composite parent) { + this.parentComposite = parent; + + // Set a clean 1-column layout on parent + GridLayout parentLayout = new GridLayout(1, true); + parentLayout.marginWidth = 0; + parentLayout.marginHeight = 0; + parentLayout.horizontalSpacing = 0; + parentLayout.verticalSpacing = 0; + parent.setLayout(parentLayout); + + // Always subscribe to events first + subscribeToEventBroker(); + + // Register ignored problems listener + ignoredStore = IgnoredProblemsStore.getInstance(); + ignoredStore.addListener(this); + + // Initial render check + refreshViewMode(); + } + + + /** + * Loads an image and scales it down to the given max width (maintaining aspect ratio) + * if it is larger than that width. + */ + private static Image createScaledImage(String path, int maxWidth) { + Image original = Activator.getImageDescriptor(path).createImage(); + if (original.getBounds().width <= maxWidth) { + return original; + } + + double scale = (double) maxWidth / original.getBounds().width; + int scaledWidth = maxWidth; + int scaledHeight = (int) Math.round(original.getBounds().height * scale); + + ImageData scaledData = original.getImageData().scaledTo(scaledWidth, scaledHeight); + Image scaledImage = new Image(original.getDevice(), scaledData); + original.dispose(); + return scaledImage; + } + + /** + * Determines which panel to draw based on current credentials status. + */ + private void refreshViewMode() { + if (parentComposite == null || parentComposite.isDisposed()) { + return; + } + + if (!PluginUtils.areCredentialsDefined()) { + drawMissingCredentialsPanel(parentComposite); + } else { + loadCachedIssues(); + drawFindingsPanel(parentComposite); + } + } + + + /** + * Loads initial cached scan issues from workspace session properties. + */ + private void loadCachedIssues() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + Map> existingIssues = problemHolder.getAllScanIssues(); + if (existingIssues != null && !existingIssues.isEmpty()) { + this.currentIssues = existingIssues; + } + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error reading cached issues: " + e.getMessage()); + } + } + + + private Composite openSettingsComposite; + + /** + * Renders the missing credentials panel centered inside the view parent. + */ + private void drawMissingCredentialsPanel(Composite parent) { + // Dispose all existing UI components in the view container + for (Control child : parent.getChildren()) { + child.dispose(); + } + + clearToolbar(); + + openSettingsComposite = new Composite(parent, SWT.NONE); + openSettingsComposite.setLayout(new GridLayout(1, true)); + openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + + // Logo + final Label cxLogo = new Label(openSettingsComposite, SWT.NONE); + cxLogo.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + cxLogo.setImage(CheckmarxView.CHECKMARX_OPEN_SETTINGS_LOGO); + + // Open Settings Button + Button btn = new Button(openSettingsComposite, SWT.NONE); + btn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + btn.setText(PluginConstants.BTN_OPEN_SETTINGS); + + btn.addListener(SWT.Selection, event -> { + PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn( + shell, "com.checkmarx.eclipse.properties.preferencespage", null, null); + if (pref != null) { + pref.open(); + } + }); + + parent.layout(true, true); + } + + private void drawFindingsPanel(Composite parent) { + // Clear out missing credentials panel if it exists + for (Control child : parent.getChildren()) { + child.dispose(); + } + + SashForm sashForm = new SashForm(parent, SWT.HORIZONTAL); + sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + sashForm.setLayout(new FillLayout()); + + Composite treeComposite = new Composite(sashForm, SWT.NONE); + treeComposite.setLayout(new FillLayout()); + + treeViewer = new TreeViewer(treeComposite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + treeViewer.setContentProvider(new FindingsContentProvider()); + treeViewer.setLabelProvider(new FindingsLabelProvider()); + + Composite promotionalComposite = new Composite(sashForm, SWT.NONE); + drawPromotionalPanel(promotionalComposite); + + sashForm.setWeights(new int[] { 70, 30 }); + + setupToolbar(); + setupTreeListeners(); + + if (!currentIssues.isEmpty()) { + refreshTreeWithFilter(); + } + + parent.layout(true, true); + } + + /** + * Renders the promotional cube image and description text in the right-hand pane + * of the findings split view. + */ + private void drawPromotionalPanel(Composite promotionalComposite) { + GridLayout layout = new GridLayout(1, false); + layout.marginLeft = 0; + layout.marginRight = 40; + layout.marginHeight = 10; + promotionalComposite.setLayout(layout); + + Label cubeLabel = new Label(promotionalComposite, SWT.NONE); + cubeLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + cubeLabel.setImage(FINDINGS_PROMOTIONAL_CUBE); + Label descriptionLabel = new Label(promotionalComposite, SWT.WRAP); + GridData descriptionData = new GridData(SWT.LEFT, SWT.CENTER, true, false); + descriptionLabel.setLayoutData(descriptionData); + descriptionLabel.setText(PluginConstants.FINDINGS_PROMO_DESCRIPTION); + + // SWT.WRAP labels need an explicit widthHint to wrap and left-align under the image + // instead of growing to one unbroken line. The pane has no real bounds yet at this + // point (the parent hasn't laid out), so compute it once asynchronously after the + // initial layout, and again whenever the pane is resized (e.g. by dragging the sash). + Runnable applyWrapWidth = () -> { + if (promotionalComposite.isDisposed()) { + return; + } + int availableWidth = promotionalComposite.getClientArea().width + - (layout.marginLeft + layout.marginRight); + if (availableWidth > 0 && descriptionData.widthHint != availableWidth) { + descriptionData.widthHint = availableWidth; + promotionalComposite.layout(true); + } + }; + + promotionalComposite.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + applyWrapWidth.run(); + } + }); + Display.getDefault().asyncExec(applyWrapWidth); + } + + /** + * Subscribes to IEventBroker for issue updates & settings changes. + */ + private void subscribeToEventBroker() { + try { + org.eclipse.e4.core.services.events.IEventBroker eventBroker = + getSite().getService(org.eclipse.e4.core.services.events.IEventBroker.class); + + if (eventBroker == null) { + eventBroker = PlatformUI.getWorkbench().getService( + org.eclipse.e4.core.services.events.IEventBroker.class); + } + + if (eventBroker != null) { + // Topic 1: Scan issues updated + eventHandler = event -> { + Object data = event.getProperty(org.eclipse.e4.core.services.events.IEventBroker.DATA); + if (data instanceof Map) { + @SuppressWarnings("unchecked") + Map> newIssues = (Map>) data; + + Display.getDefault().asyncExec(() -> { + this.currentIssues = newIssues; + if (treeViewer != null && !treeViewer.getControl().isDisposed()) { + refreshTreeWithFilter(); + } + }); + } + }; + eventBroker.subscribe(ProblemHolderService.ISSUES_UPDATED_TOPIC, eventHandler); + + // Topic 2: Settings/Credentials applied or changed + settingsEventHandler = event -> { + Display.getDefault().asyncExec(() -> { + refreshViewMode(); + }); + }; + eventBroker.subscribe(PluginConstants.TOPIC_APPLY_SETTINGS, settingsEventHandler); + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error subscribing to IEventBroker: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Override + public void dispose() { + System.out.println("[FINDINGS] Disposing CxFindingsView..."); + + // 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); + System.out.println("[FINDINGS] ✓ Unsubscribed from IEventBroker"); + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error unsubscribing from IEventBroker: " + e.getMessage()); + } + } + + // 2. Unsubscribe from IgnoredProblemsStore + if (ignoredStore != null) { + // If your IgnoredProblemsStore supports removing listeners, call it here: + // ignoredStore.removeListener(this); + } + + super.dispose(); + } + + private Composite parentComposite; + + + private void initFindingsViewUI() { + try { + System.out.println("[FINDINGS] [INIT-STEP 1/5] Getting workspace projects..."); + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder != null) { + Map> existingIssues = problemHolder.getAllScanIssues(); + if (existingIssues != null && !existingIssues.isEmpty()) { + this.currentIssues = existingIssues; + } + } + } + + subscribeToEventBroker(); + + ignoredStore = IgnoredProblemsStore.getInstance(); + ignoredStore.addListener(this); + + drawFindingsPanel(parentComposite); + + } catch (Exception e) { + System.err.println("[FINDINGS] Error during view creation: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Removes every contribution from the view toolbar. + */ + private void clearToolbar() { + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + toolbar.removeAll(); + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + } + + private void setupToolbar() { + System.out.println("[FINDINGS] Setting up toolbar with severity filters..."); + 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 = () -> { + System.out.println("[FINDINGS] Filter changed - refreshing tree"); + refreshTreeWithFilter(); + }; + + toolbar.add(new VulnerabilityFilterAction.MaliciousFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.CriticalFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.HighFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.MediumFilter(filterListener)); + toolbar.add(new VulnerabilityFilterAction.LowFilter(filterListener)); + + toolbar.add(new org.eclipse.jface.action.Separator("\t")); + + // Shared Eclipse images (replace with your own icons later) + ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); + + // Toggle Expand/Collapse action + Action toggleExpandCollapseAction = new Action("Expand All", Action.AS_PUSH_BUTTON) { + + private boolean expanded = false; + + { + setToolTipText("Collapse All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL)); + } + + @Override + public void run() { + if (expanded) { + treeViewer.collapseAll(); + setText("Expand All"); + setToolTipText("Expand All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED)); + } else { + treeViewer.expandAll(); + setText("Collapse All"); + setToolTipText("Collapse All Findings"); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL)); + } + + expanded = !expanded; + } + }; + + toolbar.add(toggleExpandCollapseAction); + + // Add spacing before preferences button + toolbar.add(new org.eclipse.jface.action.Separator("\t")); + + // Preferences action (same implementation as CheckmarxView) + Action openPreferencesPageAction = + new ActionOpenPreferencesPage( + null, + treeViewer, + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell()) + .createAction(); + + // Toolbar preferences button + Action toolbarPreferencesAction = + new Action("\u2000⋮", Action.AS_PUSH_BUTTON) { + @Override + public void run() { + openPreferencesPageAction.run(); + } + }; + + toolbarPreferencesAction.setToolTipText("Checkmarx Preferences"); + toolbar.add(toolbarPreferencesAction); + + toolbar.update(true); + getViewSite().getActionBars().updateActionBars(); + System.out.println("[FINDINGS] Toolbar configured with 5 severity filters and preferences button"); + } + private void setupTreeListeners() { + Tree tree = treeViewer.getTree(); + System.out.println("[FINDINGS] Setting up tree listeners..."); + + //Listner for redirection + tree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + System.out.println("[FINDINGS] Single-click detected"); + navigateToSelectedIssue(treeViewer.getSelection()); + } + }); + + // Right-click context menu + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(MouseEvent e) { + if (e.button == 3) { + System.out.println("[FINDINGS] Right-click detected at coordinates: " + e.x + ", " + e.y); + showContextMenu(e); + } + } + }); + + System.out.println("[FINDINGS] Tree listeners configured"); + } + + private void navigateToSelectedIssue(ISelection selection) { + System.out.println("[FINDINGS] Navigating to selected issue..."); + 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); + System.out.println("[FINDINGS] Navigating to: " + filePath + ", Line: " + location.getLine()); + openFileInEditor(filePath, location.getLine(), detail); + } else { + System.out.println("[FINDINGS] No location information available for issue: " + detail.getTitle()); + } + } + + /** + * Show detailed information about an issue. + */ + private void showIssueDetails(ScanIssue issue) { + StringBuilder details = new StringBuilder(); + details.append("\n========== ISSUE DETAILS ==========\n"); + details.append("Title: ").append(issue.getTitle()).append("\n"); + details.append("Severity: ").append(issue.getSeverity()).append("\n"); + details.append("Scan Engine: ").append(issue.getScanEngine()).append("\n"); + details.append("Description: ").append(issue.getDescription()).append("\n"); + details.append("Issue ID: ").append(issue.getScanIssueId()).append("\n"); + + if (issue.getPackageVersion() != null) { + details.append("Package Version: ").append(issue.getPackageVersion()).append("\n"); + } + if (issue.getCve() != null) { + details.append("CVE: ").append(issue.getCve()).append("\n"); + } + if (issue.getRemediationAdvise() != null) { + details.append("Remediation: ").append(issue.getRemediationAdvise()).append("\n"); + } + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location loc = issue.getLocations().get(0); + details.append("Location: Line ").append(loc.getLine()).append(", Col ").append(loc.getStartIndex()).append("\n"); + } + details.append("====================================\n"); + + System.out.println(details.toString()); + } + + /** + * Fix issue with AI Assist. + */ + private void fixWithAIAssist(ScanIssue issue) { + + try { + // Build remediation prompt based on engine type + String prompt = com.checkmarx.eclipse.devassist.ui.findings.integration.RemediationPromptBuilder + .buildRemediationPrompt(issue); + + if (prompt == null || prompt.isEmpty()) { + System.out.println("[FINDINGS] ERROR: Failed to build remediation prompt"); + showErrorNotification("Failed to build prompt for this issue type"); + return; + } + + // Send to Copilot via integration + System.out.println("[FINDINGS] Sending prompt to Copilot..."); + boolean success = com.checkmarx.eclipse.devassist.ui.findings.integration.CopilotIntegration + .sendPromptToCopilot(prompt); + + if (success) { + System.out.println("[FINDINGS] Prompt sent to Copilot successfully"); + } else { + System.out.println("[FINDINGS] ! Copilot not available, prompt in clipboard"); + } + + } catch (Exception e) { + System.out.println("[FINDINGS] ERROR: Exception in fixWithAIAssist: " + e.getMessage()); + e.printStackTrace(); + showErrorNotification("Error: " + e.getMessage()); + } + } + + /** + * Show error notification to user + */ + private void showErrorNotification(String message) { + org.eclipse.swt.widgets.MessageBox msgBox = new org.eclipse.swt.widgets.MessageBox( + treeViewer.getTree().getShell(), + org.eclipse.swt.SWT.ERROR); + msgBox.setMessage(message); + msgBox.setText("Checkmarx AI Assist"); + msgBox.open(); + } + + /** + * Ignore this specific finding and remove from the Findings View. + * The finding is added to the IgnoredProblemsStore and appears in the Ignored Problems Window. + */ + private void ignoreThisFinding(ScanIssue issue) { + + try { + // Verify store is initialized + if (ignoredStore == null) { + System.err.println("[FINDINGS] ERROR: IgnoredProblemsStore is NULL!"); + showErrorNotification("Error: IgnoredProblemsStore not initialized"); + return; + } + + System.out.println("[FINDINGS] IgnoredProblemsStore is initialized"); + + // Add to ignored store with full finding details for display in Ignored Problems View + ignoredStore.ignoreProblem(issue); + System.out.println("[FINDINGS] Added to IgnoredProblemsStore: " + issue.getScanIssueId()); + + // Check if it was actually added + boolean isIgnored = ignoredStore.isIgnored(issue.getScanIssueId()); + + // Refresh the tree to remove the ignored finding + System.out.println("[FINDINGS] Calling refreshTreeWithFilter..."); + refreshTreeWithFilter(); + System.out.println("[FINDINGS] ✓ Findings tree refreshed - finding removed"); + + System.out.println("[FINDINGS] ========================================"); + } catch (Exception e) { + System.err.println("[FINDINGS] ✗ Error ignoring finding: " + e.getMessage()); + e.printStackTrace(); + showErrorNotification("Failed to ignore finding: " + e.getMessage()); + } + } + + /** + * Ignore all findings of the same type/package. + * For OSS: ignores all findings with the same package version + * For CONTAINERS: ignores all findings with the same image tag + */ + private void ignoreAllOfType(ScanIssue issue) { + + try { + int ignoredCount = 0; + String typeIdentifier = issue.getPackageVersion() != null ? issue.getPackageVersion() : issue.getImageTag(); + + // Iterate through all current issues and ignore matching ones + for (List issues : currentIssues.values()) { + for (ScanIssue currentIssue : issues) { + // Match by same type/package/image + if (currentIssue.getScanEngine() == issue.getScanEngine()) { + String currentTypeIdentifier = currentIssue.getPackageVersion() != null ? + currentIssue.getPackageVersion() : currentIssue.getImageTag(); + + if (typeIdentifier != null && typeIdentifier.equals(currentTypeIdentifier)) { + ignoredStore.ignoreProblem(currentIssue); + ignoredCount++; + } + } + } + } + + System.out.println("[FINDINGS] ✓ Ignored " + ignoredCount + " findings of this type"); + refreshTreeWithFilter(); + System.out.println("[FINDINGS] ✓ Findings tree refreshed"); + System.out.println("[FINDINGS] ========================================"); + } 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); + System.out.println("[FINDINGS] Issue details copied to clipboard"); + System.out.println(json.toString()); + } catch (Exception e) { + System.out.println("[FINDINGS] Failed to copy to clipboard: " + e.getMessage()); + } + } + + 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 { + System.out.println("[FINDINGS] Attempting to navigate to: " + filePath + " at line " + lineNumber); + + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( + new org.eclipse.core.runtime.Path(filePath)); + + if (file == null || !file.exists()) { + System.out.println("✗ File not found in workspace: " + filePath); + return; + } + + // 1. Open file in active workbench page + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IEditorPart editor = IDE.openEditor(page, file); + System.out.println("✓ Opened file in editor: " + filePath); + + // **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) { + System.out.println("✗ Error navigating to file: " + e.getMessage()); + 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) { + System.out.println("[REALTIME-SETUP] ✗ File or editor is null"); + return; + } + + System.out.println("[REALTIME-SETUP] [STEP 1/5] Starting setup for: " + file.getName()); + System.out.println("[REALTIME-SETUP] [STEP 1/5] Editor type: " + editor.getClass().getSimpleName()); + + try { + // Extract document for real-time scanning + org.eclipse.jface.text.IDocument document = null; + String filePath = file.getLocation().toOSString(); + String fileName = file.getName(); + + System.out.println("[REALTIME-SETUP] [STEP 2/5] Extracting document from editor..."); + + // Try method 1: Direct ITextEditor instance check + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + System.out.println("[REALTIME-SETUP] [STEP 2/5] Editor is ITextEditor (direct)"); + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + + // Try method 2: ITextEditor Adapter pattern (for MavenPomEditor, etc.) + if (document == null) { + System.out.println("[REALTIME-SETUP] [STEP 2/5] Trying ITextEditor adapter pattern..."); + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor != null) { + System.out.println("[REALTIME-SETUP] [STEP 2/5] Got ITextEditor via adapter"); + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + } + + // Try method 3: Direct IDocument adapter (some editors provide this directly) + if (document == null) { + System.out.println("[REALTIME-SETUP] [STEP 2/5] Trying direct IDocument adapter..."); + document = editor.getAdapter(org.eclipse.jface.text.IDocument.class); + if (document != null) { + System.out.println("[REALTIME-SETUP] [STEP 2/5] Got IDocument directly via adapter"); + } + } + + if (document == null) { + System.out.println("[REALTIME-SETUP] ✗ [STEP 2/5] FAILED: Could not extract document from editor type: " + editor.getClass().getName()); + return; + } + + System.out.println("[REALTIME-SETUP] ✓ [STEP 2/5] Document extracted successfully"); + + System.out.println("[REALTIME-SETUP] [STEP 3/5] Creating RealTimeScanJob for: " + fileName); + + // Create a scan job for this file + com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob scanJob = + new com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob(file, fileName); + + System.out.println("[REALTIME-SETUP] ✓ [STEP 3/5] RealTimeScanJob created"); + + System.out.println("[REALTIME-SETUP] [STEP 4/5] Creating and registering document listener..."); + + // Create a document listener that reschedules the job on every keystroke + com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = null; + if (file != null) { + try { + org.eclipse.core.resources.IProject project = file.getProject(); + if (project != null) { + scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project.getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "scan-scheduler")); + } + } catch (Exception e) { + // Ignore if scheduler not available + } + } + com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxDocumentListener docListener = + new com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxDocumentListener(fileName, scanJob, file, scheduler); + + // Register the document listener + document.addDocumentListener(docListener); + System.out.println("[REALTIME-SETUP] ✓ [STEP 4/5] Document listener registered - edits will now trigger scans"); + + System.out.println("[REALTIME-SETUP] [STEP 5/5] Applying cached decorations..."); + + // Apply cached decorations if findings exist for this file + // Pass the editor directly to avoid search issues with MavenPomEditor + applyCachedDecorationsForFile(file, document, editor); + + System.out.println("[REALTIME-SETUP] ✓ [STEP 5/5] Setup complete for: " + fileName); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] ✗ EXCEPTION during setup: " + e.getMessage()); + System.err.println("[REALTIME-SETUP] Exception type: " + e.getClass().getName()); + System.err.println("[REALTIME-SETUP] Stack trace:"); + e.printStackTrace(); + } + } + + /** + * Apply cached decorations (gutter icons, underlines) when editor is opened via navigation. + * Uses the provided editor directly instead of searching for it. + */ + private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, + org.eclipse.jface.text.IDocument document, + org.eclipse.ui.IEditorPart editor) { + if (file == null || document == null || editor == null) { + return; + } + + try { + String filePath = file.getLocation().toOSString(); + org.eclipse.core.resources.IProject project = file.getProject(); + + if (project == null) { + return; + } + + // Get cached findings for this file + ProblemHolderService problemHolder = + (ProblemHolderService) project.getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder == null) { + return; + } + + java.util.List cachedIssues = problemHolder.getScanIssuesByFile(filePath); + + if (cachedIssues == null || cachedIssues.isEmpty()) { + System.out.println("[REALTIME-SETUP] No cached findings for: " + file.getName()); + return; + } + + // Apply decorations directly using the provided editor + System.out.println("[REALTIME-SETUP] ✓ Applying " + cachedIssues.size() + " cached decorations for: " + file.getName()); + applyDecorationsDirectly(editor, file, cachedIssues); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] Error applying cached decorations: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Apply decorations directly to the provided editor without searching for it. + * This avoids issues with MavenPomEditor not being found by IFile comparison. + */ + private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, + org.eclipse.core.resources.IFile file, + java.util.List scanIssues) { + if (editor == null || file == null || scanIssues == null || scanIssues.isEmpty()) { + return; + } + + try { + // Get the text editor + org.eclipse.ui.texteditor.ITextEditor textEditor = + editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + + if (textEditor == null) { + System.out.println("[REALTIME-SETUP-DIRECT] ✗ Cannot adapt editor to ITextEditor"); + return; + } + + // Get document provider and input + org.eclipse.ui.texteditor.IDocumentProvider docProvider = textEditor.getDocumentProvider(); + if (docProvider == null) { + System.out.println("[REALTIME-SETUP-DIRECT] ✗ No document provider for editor"); + return; + } + + // Get annotation model from the document provider (proper way for all editor types) + org.eclipse.jface.text.source.IAnnotationModel annotationModel = + docProvider.getAnnotationModel(textEditor.getEditorInput()); + + if (annotationModel == null) { + System.out.println("[REALTIME-SETUP-DIRECT] ✗ No annotation model from provider"); + return; + } + + System.out.println("[REALTIME-SETUP-DIRECT] ✓ Got annotation model, applying " + scanIssues.size() + " decorations"); + + // Get document from provider + org.eclipse.jface.text.IDocument document = docProvider.getDocument(textEditor.getEditorInput()); + + if (document == null) { + System.out.println("[REALTIME-SETUP-DIRECT] ✗ Cannot get document from provider"); + return; + } + + // Apply each issue's decoration using OSS-specific logic + java.util.List annotations = + new java.util.ArrayList<>(); + + for (ScanIssue issue : scanIssues) { + try { + // Create annotation + com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation annotation = + createAnnotationForIssue(issue); + + if (annotation == null) { + continue; + } + + // Calculate position (OSS = first line only) + org.eclipse.jface.text.Position pos = calculatePositionForIssue(document, issue); + + if (pos != null && pos.getLength() > 0) { + annotationModel.addAnnotation(annotation, pos); + annotations.add(annotation); + System.out.println("[REALTIME-SETUP-DIRECT] ✓ Added annotation for: " + issue.getTitle()); + } + } catch (Exception e) { + System.err.println("[REALTIME-SETUP-DIRECT] Error decorating issue: " + e.getMessage()); + } + } + + System.out.println("[REALTIME-SETUP-DIRECT] ✓ Applied " + annotations.size() + " decorations successfully"); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP-DIRECT] ✗ Error applying decorations directly: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Create annotation for an issue. + */ + private com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation createAnnotationForIssue(ScanIssue issue) { + try { + String annotationType = mapSeverityToAnnotationType(issue.getSeverity()); + return new com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation( + annotationType, + issue.getTitle(), + issue.getDescription() + ); + } catch (Exception e) { + return null; + } + } + + /** + * Map severity to annotation type. + * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. + */ + private String mapSeverityToAnnotationType(String severity) { + if (severity == null) { + return "com.checkmarx.eclipse.findings.unknown"; + } + String upper = severity.toUpperCase(); + if (upper.contains("MALICIOUS")) { + return "com.checkmarx.eclipse.findings.malicious"; + } + if (upper.contains("CRITICAL") || upper.contains("ERROR")) { + return "com.checkmarx.eclipse.findings.critical"; + } + if (upper.contains("HIGH")) { + return "com.checkmarx.eclipse.findings.high"; + } + if (upper.contains("MEDIUM")) { + return "com.checkmarx.eclipse.findings.medium"; + } + if (upper.contains("LOW") || upper.contains("INFO")) { + return "com.checkmarx.eclipse.findings.low"; + } + if (upper.contains("UNKNOWN")) { + return "com.checkmarx.eclipse.findings.unknown"; + } + if (upper.contains("OK")) { + return "com.checkmarx.eclipse.findings.ok"; + } + if (upper.contains("IGNORED")) { + return "com.checkmarx.eclipse.findings.ignored"; + } + return "com.checkmarx.eclipse.findings.unknown"; + } + + /** + * Calculate position for an issue (OSS = first line only). + */ + private org.eclipse.jface.text.Position calculatePositionForIssue(org.eclipse.jface.text.IDocument document, ScanIssue issue) { + try { + if (issue.getLocations() == null || issue.getLocations().isEmpty()) { + return null; + } + + com.checkmarx.eclipse.devassist.model.Location location = issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // 0-based + + int lineCount = document.getNumberOfLines(); + if (lineNumber < 0 || lineNumber >= lineCount) { + return null; + } + + org.eclipse.jface.text.IRegion lineInfo = document.getLineInformation(lineNumber); + int offset = lineInfo.getOffset(); + int length = lineInfo.getLength(); + + // FIX: Skip leading whitespace to match ProblemDecorator.calculateRange() + int trimOffset = getLeadingWhitespaceOffset(document, offset, length); + int adjustedOffset = offset + trimOffset; + int adjustedLength = Math.max(1, length - trimOffset); + + return new org.eclipse.jface.text.Position(adjustedOffset, adjustedLength); + + } catch (Exception e) { + return null; + } + } + + /** + * Scroll editor to specific line number using native Eclipse ITextEditor adapter. + */ + private boolean scrollToLine(IEditorPart editor, int lineNumber) { + if (editor == null || lineNumber <= 0) return false; + + try { + // Use Eclipse's standard adapter pattern instead of reflection + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor == null && editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + } + + if (textEditor != null) { + org.eclipse.ui.texteditor.IDocumentProvider provider = textEditor.getDocumentProvider(); + if (provider != null) { + org.eclipse.jface.text.IDocument document = provider.getDocument(textEditor.getEditorInput()); + if (document != null && lineNumber <= document.getNumberOfLines()) { + // Line numbers in IDocument are 0-indexed + int lineOffset = document.getLineOffset(lineNumber - 1); + textEditor.selectAndReveal(lineOffset, 0); + System.out.println("[FINDINGS] ✓ Successfully scrolled to line " + lineNumber); + return true; + } + } + } + } catch (Exception e) { + System.out.println("[FINDINGS] Line scrolling via adapter failed: " + e.getMessage()); + } + return false; + } + + /** + * Ensures IMarker.LINE_NUMBER is explicitly set as a 1-based Integer attribute. + */ + private void createMarkerForIssue(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + + try { + IMarker existingMarker = findMarkerForIssue(file, issue); + if (existingMarker != null && existingMarker.exists()) { + return; + } + + // 1. Create the marker using the declared ID + IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); + + // 2. Set Standard Core Eclipse Attributes (CRITICAL for Quick Fix matching) + int lineNumber = issue.getLocations().get(0).getLine(); + newMarker.setAttribute(IMarker.LINE_NUMBER, lineNumber > 0 ? lineNumber : 1); + newMarker.setAttribute(IMarker.MESSAGE, issue.getTitle() != null ? issue.getTitle() : "Checkmarx Finding"); + newMarker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING); + newMarker.setAttribute(IMarker.USER_EDITABLE, false); + + // FIX: Set character offsets with whitespace trimming (so underline doesn't include leading spaces) + setMarkerCharacterOffsets(newMarker, file, lineNumber); + + // 3. Populate custom attributes + com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); + + System.out.println("[FINDINGS] ✓ Created marker with LINE_NUMBER=" + lineNumber + " and MESSAGE=" + issue.getTitle()); + + } catch (org.eclipse.core.runtime.CoreException e) { + System.err.println("[FINDINGS] Error creating marker: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Apply highlighting to the problematic line. + */ + /** + * Navigate to the marker that corresponds to this issue. + * JDT's editor will automatically underline the marker and respect + * the marker annotation infrastructure (no custom hover registration needed). + */ + private void highlightViaMarker(org.eclipse.ui.IEditorPart editor, IFile file, ScanIssue issue) { + try { + if (editor == null || file == null || issue == null) { + return; + } + + // Find the marker corresponding to this issue + IMarker marker = findMarkerForIssue(file, issue); + if (marker != null && marker.exists()) { + org.eclipse.ui.ide.IDE.gotoMarker(editor, marker); + System.out.println("[FINDINGS] ✓ Navigated to marker for: " + issue.getTitle()); + } else { + System.out.println("[FINDINGS] No marker found for issue: " + issue.getTitle()); + } + } catch (Exception e) { + System.out.println("[FINDINGS] Error navigating to marker: " + e.getMessage()); + } + } + + /** + * Find the IMarker that corresponds to a ScanIssue. + * Matches by file, line number, and optionally title. + */ + private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { + if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return null; + } + + int issueLine = issue.getLocations().get(0).getLine(); + String issueTitle = issue.getTitle(); + + try { + IMarker[] markers = file.findMarkers("com.checkmarx.eclipse.plugin.checkmarxProblemMarker", true, org.eclipse.core.resources.IResource.DEPTH_ZERO); + for (IMarker marker : markers) { + int markerLine = marker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); + if (markerLine == issueLine) { + // Optional: also match by message prefix for better accuracy + String markerMsg = marker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); + if (issueTitle == null || issueTitle.isEmpty() || markerMsg.contains(issueTitle)) { + return marker; + } + } + } + } catch (Exception e) { + System.out.println("[FINDINGS] Error finding marker for issue: " + e.getMessage()); + } + + return null; + } + + /** + * Create a marker for a ScanIssue. + * + * **CRITICAL FIX**: Markers were never being created, only searched for. + * This method creates markers on-demand when user navigates to an issue. + * + * **Works for ALL file types**: Java, Python, C++, JavaScript, YAML, XML, etc. + * Uses Eclipse's universal IMarker API (not language-specific). + * + * Marker attributes are populated using MarkerIssueMapper to store + * all ScanIssue data in marker attributes for later retrieval. + * + * @param file File to create marker in + * @param issue ScanIssue to create marker for + */ +// private void createMarkerForIssue(IFile file, ScanIssue issue) { +// if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { +// System.out.println("[FINDINGS] [MARKER-CREATE] ✗ Missing file, issue, or locations"); +// return; +// } +// +// try { +// System.out.println("[FINDINGS] [MARKER-CREATE] ╔═══════════════════════════════════════╗"); +// System.out.println("[FINDINGS] [MARKER-CREATE] â•‘ Creating marker for ScanIssue â•‘"); +// System.out.println("[FINDINGS] [MARKER-CREATE] ╚═══════════════════════════════════════╝"); +// System.out.println("[FINDINGS] [MARKER-CREATE] File: " + file.getFullPath()); +// System.out.println("[FINDINGS] [MARKER-CREATE] Issue: " + issue.getTitle()); +// System.out.println("[FINDINGS] [MARKER-CREATE] Engine: " + issue.getScanEngine()); +// System.out.println("[FINDINGS] [MARKER-CREATE] Line: " + issue.getLocations().get(0).getLine()); +// +// // Step 1: Check if marker already exists for this issue +// IMarker existingMarker = findMarkerForIssue(file, issue); +// if (existingMarker != null && existingMarker.exists()) { +// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker already exists, skipping creation"); +// return; +// } +// +// // Step 2: Create new marker using Eclipse's universal IMarker API +// // **KEY**: Uses IMarker.PROBLEM which works for ALL file types +// // - NOT language-specific (works for Java, Python, C++, JS, YAML, etc.) +// // - Marker appears in Eclipse's Problems View +// // - Can be navigated with IDE.gotoMarker() +// IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); +// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker created"); +// +// // Step 3: Populate marker attributes using MarkerIssueMapper +// // This stores all ScanIssue data in marker for later retrieval +// com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); +// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker populated with issue data"); +// +// // Step 4: Verify marker creation +// if (newMarker.exists()) { +// String markerMsg = newMarker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); +// int markerLine = newMarker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); +// int markerSeverity = newMarker.getAttribute(org.eclipse.core.resources.IMarker.SEVERITY, -1); +// +// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker verified:"); +// System.out.println("[FINDINGS] [MARKER-CREATE] ID: " + newMarker.getId()); +// System.out.println("[FINDINGS] [MARKER-CREATE] Message: " + markerMsg); +// System.out.println("[FINDINGS] [MARKER-CREATE] Line: " + markerLine); +// System.out.println("[FINDINGS] [MARKER-CREATE] Severity: " + markerSeverity); +// System.out.println("[FINDINGS] [MARKER-CREATE] ═════════════════════════════════════════"); +// } else { +// System.out.println("[FINDINGS] [MARKER-CREATE] ✗ Failed to create marker!"); +// } +// +// } catch (org.eclipse.core.runtime.CoreException e) { +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ CoreException creating marker: " + e.getMessage()); +// e.printStackTrace(); +// } catch (Exception e) { +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ Error creating marker: " + e.getMessage()); +// e.printStackTrace(); +// } +// } + + private void showContextMenu(MouseEvent e) { + ISelection selection = treeViewer.getSelection(); + if (!(selection instanceof IStructuredSelection)) { + System.out.println("[FINDINGS] Invalid selection for context menu"); + return; + } + + IStructuredSelection ssel = (IStructuredSelection) selection; + Object element = ssel.getFirstElement(); + + if (!(element instanceof ScanDetailWithPath)) { + System.out.println("[FINDINGS] Context menu: Selected element is not a ScanDetailWithPath"); + return; + } + + ScanDetailWithPath detailWithPath = (ScanDetailWithPath) element; + ScanIssue issue = detailWithPath.getDetail(); + + System.out.println("[FINDINGS] Creating context menu for: " + issue.getTitle()); + + org.eclipse.swt.widgets.Menu menu = new org.eclipse.swt.widgets.Menu(treeViewer.getTree()); + + // Menu Item 1: View Details + org.eclipse.swt.widgets.MenuItem viewDetailsItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + viewDetailsItem.setText("View Details"); + viewDetailsItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + System.out.println("[FINDINGS] Action: View Details - Issue: " + issue.getTitle()); + showIssueDetails(issue); + } + }); + + // Menu Item 2: Fix with AI Assist + org.eclipse.swt.widgets.MenuItem fixWithAIItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + fixWithAIItem.setText("Fix with AI Assist"); + fixWithAIItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + System.out.println("[FINDINGS] Action: Fix with AI Assist - Issue: " + issue.getTitle()); + fixWithAIAssist(issue); + } + }); + + // Menu Item 3: Ignore This Finding + org.eclipse.swt.widgets.MenuItem ignoreItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + ignoreItem.setText("Ignore This Finding"); + ignoreItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + System.out.println("[FINDINGS] Action: Ignore This Finding - Issue: " + issue.getTitle()); + ignoreThisFinding(issue); + } + }); + + // Menu Item 4: Ignore All of This Type (for OSS and CONTAINERS) + if (issue.getScanEngine() == ScanEngine.OSS || issue.getScanEngine() == ScanEngine.CONTAINERS) { + org.eclipse.swt.widgets.MenuItem ignoreAllItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + ignoreAllItem.setText("Ignore All of This Type"); + ignoreAllItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { + System.out.println("[FINDINGS] Action: Ignore All of This Type - Issue Type: " + issue.getTitle()); + 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) { + System.out.println("[FINDINGS] Action: Copy Issue Details - Issue: " + issue.getTitle()); + 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) { + System.out.println("[FINDINGS] Action: Navigate to Line - File: " + detailWithPath.getFilePath()); + navigateToIssue(detailWithPath); + } + }); + + menu.setLocation(treeViewer.getTree().toDisplay(e.x, e.y)); + menu.setVisible(true); + } + + private void refreshTreeWithFilter() { + System.out.println("[FINDINGS] ========== REFRESH TREE START =========="); + System.out.println("[FINDINGS] Refreshing tree with active filters and ignored problems..."); + System.out.println("[FINDINGS] Current issues: " + currentIssues.size() + " files"); + + // Apply active filters and refresh + VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); + System.out.println("[FINDINGS] Active filters: " + filterState.getFilters()); + System.out.println("[FINDINGS] Ignored IDs in store: " + + (ignoredStore != null ? ignoredStore.getIgnoredProblemIds() : "[]")); + + Map> filteredIssues = new HashMap<>(); + int totalBefore = 0; + int totalAfter = 0; + + for (String filePath : currentIssues.keySet()) { + List issues = currentIssues.get(filePath); + if (issues == null) continue; + + totalBefore += issues.size(); + List filtered = new java.util.ArrayList<>(); + + for (ScanIssue issue : issues) { + // ✅ Safe null guard FIRST before calling any methods on issue + if (issue == null || issue.getSeverity() == null) { + System.out.println("[FINDINGS] WARNING: Null issue or severity detected"); + continue; + } + + String issueId = issue.getScanIssueId(); + boolean isIgnored = ignoredStore != null && ignoredStore.isIgnored(issueId); + boolean hasFilter = filterState.hasFilter(issue.getSeverity()); + boolean isProblem = com.checkmarx.eclipse.devassist.backend.DevAssistUtils.isProblem(issue.getSeverity()); + + System.out.println("[FINDINGS] Issue: " + issue.getTitle() + + " | ID: " + issueId + + " | Ignored: " + isIgnored + + " | HasFilter: " + hasFilter + + " | IsProblem: " + isProblem); + + // Filter by OK/UNKNOWN/IGNORED severity (Phase 3) + if (!isProblem) { + System.out.println("[FINDINGS] -> Filtered out because severity is OK/UNKNOWN/IGNORED"); + continue; + } + // Filter by severity preference + if (!hasFilter) { + System.out.println("[FINDINGS] -> Filtered out by severity"); + continue; + } + // Filter out ignored problems + if (isIgnored) { + System.out.println("[FINDINGS] -> Filtered out because IGNORED"); + continue; + } + + System.out.println("[FINDINGS] -> KEEPING"); + filtered.add(issue); + } + + if (!filtered.isEmpty()) { + filteredIssues.put(filePath, filtered); + totalAfter += filtered.size(); + } + } + + System.out.println("[FINDINGS] Total issues before filtering: " + totalBefore); + System.out.println("[FINDINGS] Total issues after filtering: " + totalAfter); + System.out.println("[FINDINGS] Filtered issues map: " + filteredIssues.size() + " files"); + + // ✅ Verify treeViewer control before manipulating UI + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + System.out.println("[FINDINGS] Setting tree input..."); + treeViewer.setInput(filteredIssues); + System.out.println("[FINDINGS] Expanding all nodes..."); + treeViewer.expandAll(); + } + + System.out.println("[FINDINGS] ========== REFRESH TREE END =========="); + } + + /** + * Refresh the tree with new issues. Safely dispatches to the SWT UI Thread. + * + * @param issues Map of file paths to list of scan issues + */ + public void refreshTree(Map> issues) { + if (issues == null) return; + + System.out.println("[FINDINGS] ╔════════════════════════════════════════════╗"); + System.out.println("[FINDINGS] â•‘ FINDINGS VIEW: REFRESH TREE â•‘"); + System.out.println("[FINDINGS] ╚════════════════════════════════════════════╝"); + System.out.println("[FINDINGS] Input: " + issues.size() + " files"); + int totalIssues = issues.values().stream().filter(java.util.Objects::nonNull).mapToInt(List::size).sum(); + System.out.println("[FINDINGS] Total Issues: " + totalIssues); + + // Log issues by severity + Map severityCounts = new HashMap<>(); + issues.values().forEach(issueList -> { + if (issueList != null) { + issueList.forEach(issue -> { + if (issue != null && issue.getSeverity() != null) { + String severity = issue.getSeverity().toLowerCase(); + severityCounts.put(severity, severityCounts.getOrDefault(severity, 0L) + 1); + } + }); + } + }); + + System.out.println("[FINDINGS] Severity breakdown:"); + severityCounts.forEach((severity, count) -> + System.out.println("[FINDINGS] - " + severity + ": " + count) + ); + + for (String filePath : issues.keySet()) { + List fileIssues = issues.get(filePath); + System.out.println("[FINDINGS] File: " + filePath + " → " + (fileIssues != null ? fileIssues.size() : 0) + " issues"); + } + + System.out.println("[FINDINGS] Setting currentIssues and dispatching UI update..."); + 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(); + } + }); + + System.out.println("[FINDINGS] ════════════════════════════════════════════"); + } + + @Override + public void setFocus() { + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + treeViewer.getControl().setFocus(); + } + } + + public TreeViewer getTreeViewer() { + return treeViewer; + } + + /** + * Listener implementation: called when ignored problems are restored or cleared. + */ + @Override + public void onIgnoredProblemsChanged() { + System.out.println("[FINDINGS] Ignored problems changed - refreshing findings tree"); + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { + treeViewer.getControl().getDisplay().asyncExec(this::refreshTreeWithFilter); + } + } + + + private void setMarkerCharacterOffsets(IMarker marker, IFile file, int lineNumber) { + try { + org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window == null) return; + org.eclipse.ui.IWorkbenchPage page = window.getActivePage(); + if (page == null) return; + org.eclipse.ui.IEditorPart editor = page.getActiveEditor(); + if (editor == null) return; + + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor == null) return; + + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + if (doc == null || lineNumber <= 0 || lineNumber > doc.getNumberOfLines()) return; + + int lineIdx = lineNumber - 1; + int lineOffset = doc.getLineOffset(lineIdx); + int lineLen = doc.getLineLength(lineIdx); + + int trimOffset = getLeadingWhitespaceOffset(doc, lineOffset, lineLen); + marker.setAttribute(IMarker.CHAR_START, lineOffset + trimOffset); + marker.setAttribute(IMarker.CHAR_END, lineOffset + lineLen); + + } catch (Exception e) { + // If we can't set char offsets, marker will still work with line-based positioning + } + } + + private int getLeadingWhitespaceOffset(org.eclipse.jface.text.IDocument document, int lineOffset, int lineLength) { + try { + String lineText = document.get(lineOffset, lineLength); + int count = 0; + for (int i = 0; i < lineText.length(); i++) { + if (!Character.isWhitespace(lineText.charAt(i))) break; + count++; + } + return count; + } catch (Exception e) { + return 0; + } + } + +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java new file mode 100644 index 00000000..028e9561 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java @@ -0,0 +1,80 @@ +package com.checkmarx.eclipse.devassist.ui.findings.actions; + +import org.eclipse.jface.action.Action; + +/** + * Base toggle action for severity filters in the Findings view. + */ +public abstract class VulnerabilityFilterAction extends Action { + + private final String severity; + private final IFilterChangeListener filterChangeListener; + + public interface IFilterChangeListener { + void onFilterChanged(); + } + + public VulnerabilityFilterAction(String severity, IFilterChangeListener listener) { + super(severity, Action.AS_CHECK_BOX); + this.severity = severity; + this.filterChangeListener = listener; + + setText(severity.substring(0, 1).toUpperCase() + severity.substring(1)); + setImageDescriptor(org.eclipse.ui.plugin.AbstractUIPlugin + .imageDescriptorFromPlugin("com.checkmarx.eclipse.plugin", + "icons/severity/" + severity + "_20.svg")); + setToolTipText("Filter " + severity + " severity findings"); + + // Set initial state + setChecked(VulnerabilityFilterState.getInstance().hasFilter(severity)); + } + + @Override + public void run() { + VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); + if (isChecked()) { + filterState.addFilter(severity); + } else { + filterState.removeFilter(severity); + } + + if (filterChangeListener != null) { + filterChangeListener.onFilterChanged(); + } + } + + public String getSeverity() { + return severity; + } + + // Concrete implementations for each severity level + public static class MaliciousFilter extends VulnerabilityFilterAction { + public MaliciousFilter(IFilterChangeListener listener) { + super("malicious", listener); + } + } + + public static class CriticalFilter extends VulnerabilityFilterAction { + public CriticalFilter(IFilterChangeListener listener) { + super("critical", listener); + } + } + + public static class HighFilter extends VulnerabilityFilterAction { + public HighFilter(IFilterChangeListener listener) { + super("high", listener); + } + } + + public static class MediumFilter extends VulnerabilityFilterAction { + public MediumFilter(IFilterChangeListener listener) { + super("medium", listener); + } + } + + public static class LowFilter extends VulnerabilityFilterAction { + public LowFilter(IFilterChangeListener listener) { + super("low", listener); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java new file mode 100644 index 00000000..d8cf09fb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java @@ -0,0 +1,67 @@ +package com.checkmarx.eclipse.devassist.ui.findings.actions; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Holds filter state (set of severity levels) for the Findings view. + * Singleton pattern for managing active filters across the application. + */ +public class VulnerabilityFilterState { + + private static final VulnerabilityFilterState INSTANCE = new VulnerabilityFilterState(); + + private final Set selectedFilters = Collections.synchronizedSet(new HashSet<>()); + + private VulnerabilityFilterState() { + // Initialize with default filters (all severities) + selectedFilters.add("malicious"); + selectedFilters.add("critical"); + selectedFilters.add("high"); + selectedFilters.add("medium"); + selectedFilters.add("low"); + } + + public static VulnerabilityFilterState getInstance() { + return INSTANCE; + } + + public Set getFilters() { + return selectedFilters; + } + + public void addFilter(String severity) { + if (severity != null) { + selectedFilters.add(severity.toLowerCase()); + } + } + + public void removeFilter(String severity) { + if (severity != null) { + selectedFilters.remove(severity.toLowerCase()); + } + } + + public boolean hasFilter(String severity) { + if (severity == null) { + System.out.println("[FILTER] WARNING: Null severity passed to hasFilter"); + return false; + } + boolean result = selectedFilters.contains(severity.toLowerCase()); + return result; + } + + public void clearFilters() { + selectedFilters.clear(); + } + + public void resetToDefaults() { + selectedFilters.clear(); + selectedFilters.add("malicious"); + selectedFilters.add("critical"); + selectedFilters.add("high"); + selectedFilters.add("medium"); + selectedFilters.add("low"); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java new file mode 100644 index 00000000..5dc0a5f1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/dialogs/ProblemDescription.java @@ -0,0 +1,181 @@ +package com.checkmarx.eclipse.devassist.ui.findings.dialogs; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Responsible for handling and formatting descriptions of scan issues. + * Provides utility methods to construct and format messages for different issue types. + * Uses HTML formatting for rich text display. + */ +public final class ProblemDescription { + + private static final String TITLE_FONT_SIZE = "font-size:11px;"; + private static final String TITLE_FONT_FAMILY = "font-family: menlo;"; + private static final String CELL_LINE_HEIGHT_STYLE = "line-height:16px;vertical-align:middle;"; + private static final String SECONDARY_SPAN_STYLE = "display:inline-block;vertical-align:middle;line-height:16px;font-size:11px;color:#ADADAD;"; + + private static final String TABLE_WITH_TR = ""; + + /** + * Formats a description for the given scan issue. + * + * @param scanIssue the ScanIssue object + * @return formatted HTML description + */ + public String formatDescription(ScanIssue scanIssue) { + StringBuilder descBuilder = new StringBuilder(); + descBuilder.append(""); + + switch (scanIssue.getScanEngine()) { + case OSS: + buildOSSDescription(descBuilder, scanIssue); + break; + case ASCA: + buildASCADescription(descBuilder, scanIssue); + break; + case SECRETS: + buildSecretsDescription(descBuilder, scanIssue); + break; + case IAC: + buildIACDescription(descBuilder, scanIssue); + break; + case CONTAINERS: + buildContainerDescription(descBuilder, scanIssue); + break; + default: + buildDefaultDescription(descBuilder, scanIssue); + } + + descBuilder.append(""); + return descBuilder.toString(); + } + + private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append(TABLE_WITH_TR) + .append("") + .append("
") + .append("

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

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

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

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

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

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

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

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

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

"); + } + } + } + + private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append("
").append(escapeHtml(scanIssue.getTitle())).append(" -") + .append(escapeHtml(scanIssue.getDescription())).append("
"); + } + + private void buildVulnerabilitySection(StringBuilder descBuilder, ScanIssue scanIssue) { + List vulnerabilityList = scanIssue.getVulnerabilities(); + if (vulnerabilityList == null || vulnerabilityList.isEmpty()) { + return; + } + + descBuilder.append("
").append(TABLE_WITH_TR); + Map vulnerabilityCount = vulnerabilityList.stream() + .map(Vulnerability::getSeverity) + .collect(Collectors.groupingBy(severity -> severity, Collectors.counting())); + + vulnerabilityCount.forEach((severity, count) -> { + descBuilder.append("") + .append("") + .append(count).append(""); + }); + + descBuilder.append("
"); + } + + /** + * Formats a kebab-case title into Title-Case. + */ + private String formatTitle(String title) { + if (title == null || title.isEmpty()) { + return ""; + } + return Arrays.stream(title.split("-")) + .map(word -> word.isEmpty() ? "" : Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase()) + .collect(Collectors.joining("-")); + } + + /** + * Escape HTML special characters. + */ + private String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java new file mode 100644 index 00000000..27a375ba --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java @@ -0,0 +1,194 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.editor; +// +//import org.eclipse.jface.text.IInformationControl; +//import org.eclipse.jface.text.IInformationControlCreator; +//import org.eclipse.jface.text.IRegion; +//import org.eclipse.jface.text.ITextHover; +//import org.eclipse.jface.text.ITextHoverExtension; +//import org.eclipse.jface.text.ITextHoverExtension2; +//import org.eclipse.jface.text.ITextViewer; +//import org.eclipse.jface.text.Region; +//import org.eclipse.jface.text.source.Annotation; +//import org.eclipse.jface.text.source.IAnnotationModel; +//import org.eclipse.ui.IEditorPart; +//import org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover; +// +///** +// * Custom hover for Checkmarx Findings annotations in the editor. +// * +// * Finds FindingsAnnotation objects at the hover offset and displays +// * detailed vulnerability information via CxFindingsHoverControl. +// * +// * Works independently of Eclipse markers - uses ScanIssue annotation model. +// */ +//public class CxFindingsHover implements IJavaEditorTextHover, ITextHover, ITextHoverExtension, ITextHoverExtension2 { +// +// private FindingsAnnotation currentAnnotation; +// public CxFindingsHover() { +// // Default constructor for Eclipse instantiation +// } +// +// @Override +// public void setEditor(IEditorPart editor) { +// } +// +// @Override +// public String getHoverInfo(ITextViewer viewer, IRegion hoverRegion) { +// return null; +// } +// +// @Override +// public Object getHoverInfo2(ITextViewer viewer, IRegion hoverRegion) { +// System.out.println("[CX-FINDINGS-HOVER] getHoverInfo2 called"); +// return this.currentAnnotation; +// } +// +// @Override +// public IInformationControlCreator getHoverControlCreator() { +// return new IInformationControlCreator() { +// @Override +// public IInformationControl createInformationControl(org.eclipse.swt.widgets.Shell parent) { +// System.out.println("[CX-FINDINGS-HOVER] Creating hover control for annotation: " + currentAnnotation); +// if (currentAnnotation != null) { +// return new CxFindingsHoverControl(parent, currentAnnotation); +// } +// return null; +// } +// }; +// } +// +// @Override +// public IRegion getHoverRegion(ITextViewer viewer, int offset) { +// System.out.println("[CX-FINDINGS-HOVER] getHoverRegion called at offset: " + offset); +// try { +// // Find FindingsAnnotation at this offset +// FindingsAnnotation annotation = findAnnotationContainingOffset(viewer, offset); +// if (annotation == null) { +// System.out.println("[CX-FINDINGS-HOVER] No annotation found at offset " + offset); +// return null; +// } +// +// // Cache the annotation for getHoverInfo2() +// this.currentAnnotation = annotation; +// System.out.println("[CX-FINDINGS-HOVER] ✓ Found annotation: " + annotation.getTitle()); +// +// // Return the region covered by the annotation in the annotation model +// if (viewer instanceof org.eclipse.jface.text.source.ISourceViewer) { +// org.eclipse.jface.text.source.ISourceViewer sourceViewer = +// (org.eclipse.jface.text.source.ISourceViewer) viewer; +// IAnnotationModel annotationModel = sourceViewer.getAnnotationModel(); +// if (annotationModel != null) { +// org.eclipse.jface.text.Position pos = annotationModel.getPosition(annotation); +// if (pos != null) { +// System.out.println("[CX-FINDINGS-HOVER] ✓ Returning region: " + pos.getOffset() + "-" + (pos.getOffset() + pos.getLength())); +// return new Region(pos.getOffset(), pos.getLength()); +// } +// } +// } +// } catch (Exception e) { +// System.err.println("[CX-FINDINGS-HOVER] Error in getHoverRegion: " + e.getMessage()); +// } +// return null; +// } +// +// /** +// * Find a FindingsAnnotation whose position contains the given offset. +// * If multiple annotations overlap, returns the innermost (smallest range). +// */ +// private FindingsAnnotation findAnnotationContainingOffset(ITextViewer viewer, int offset) { +// try { +// if (viewer == null) { +// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: viewer is null"); +// return null; +// } +// +// IAnnotationModel annotationModel = null; +// if (viewer instanceof org.eclipse.jface.text.source.ISourceViewer) { +// annotationModel = ((org.eclipse.jface.text.source.ISourceViewer) viewer).getAnnotationModel(); +// } +// +// if (annotationModel == null) { +// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: annotation model is null"); +// return null; +// } +// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: Searching annotations..."); +// +// FindingsAnnotation bestAnnotation = null; +// int smallestRange = Integer.MAX_VALUE; +// +// // Iterate through all annotations in the model +// @SuppressWarnings("unchecked") +// java.util.Iterator iterator = annotationModel.getAnnotationIterator(); +// while (iterator.hasNext()) { +// Annotation annotation = iterator.next(); +// +// if (annotation instanceof FindingsAnnotation) { +// FindingsAnnotation findingsAnnotation = (FindingsAnnotation) annotation; +// org.eclipse.jface.text.Position pos = annotationModel.getPosition(annotation); +// +// if (pos != null) { +// int start = pos.getOffset(); +// int end = pos.getOffset() + pos.getLength(); +// +// System.out.println("[CX-FINDINGS-HOVER] Annotation: " + findingsAnnotation.getTitle() + +// " range=[" + start + "-" + end + "]"); +// +// // Check if offset falls within this annotation's range +// if (start <= offset && offset < end) { +// int range = pos.getLength(); +// System.out.println("[CX-FINDINGS-HOVER] ✓ Offset " + offset + " is INSIDE range, size=" + range); +// if (range < smallestRange) { +// smallestRange = range; +// bestAnnotation = findingsAnnotation; +// System.out.println("[CX-FINDINGS-HOVER] ✓ Selected as best annotation (innermost)"); +// } +// } else { +// System.out.println("[CX-FINDINGS-HOVER] ✗ Offset " + offset + " is OUTSIDE range"); +// } +// } +// } +// } +// +// if (bestAnnotation != null) { +// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: ✓ FOUND annotation"); +// } else { +// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: ✗ NO annotation found"); +// } +// +// return bestAnnotation; +// } catch (Exception e) { +// System.err.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: EXCEPTION - " + e.getMessage()); +// e.printStackTrace(); +// return null; +// } +// } +//} + +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.*; +import org.eclipse.swt.widgets.Shell; + +public class CxFindingsHover implements ITextHover, ITextHoverExtension { + + @Override + public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { + return null; // Not used when ITextHoverExtension is implemented + } + + @Override + public IRegion getHoverRegion(ITextViewer textViewer, int offset) { + return new Region(offset, 0); + } + + @Override + public IInformationControlCreator getHoverControlCreator() { + return new AbstractReusableInformationControlCreator() { + @Override + protected IInformationControl doCreateInformationControl(Shell parent) { + // Returns custom popup window containing real SWT Buttons + return new CxFindingsInformationControl(parent); + } + }; + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java new file mode 100644 index 00000000..86a98aaf --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java @@ -0,0 +1,254 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.AbstractInformationControl; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Custom hover control for Checkmarx Findings. + * + * Displays: + * - Issue severity badge with color + * - Issue title/message + * - Line number + * - Action buttons (Ignore, View Details, etc.) + * + * Includes auto-close timer that pauses on mouse hover. + */ +public class CxFindingsHoverControl extends AbstractInformationControl { + + private FindingsAnnotation annotation; + private Composite mainComposite; + private Timer closeTimer; + + public CxFindingsHoverControl(Shell parent, FindingsAnnotation annotation) { + super(parent, true); + this.annotation = annotation; + System.out.println("[FINDINGS-HOVER] CxFindingsHoverControl created"); + create(); + } + + @Override + public boolean hasContents() { + return annotation != null && annotation.getTitle() != null; + } + + @Override + protected void createContent(Composite parent) { + System.out.println("[FINDINGS-HOVER] Creating hover content..."); + + mainComposite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(1, false); + layout.marginHeight = 10; + layout.marginWidth = 10; + layout.verticalSpacing = 8; + mainComposite.setLayout(layout); + mainComposite.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); + + try { + // 1. Severity Badge + createSeverityBadge(); + + // 2. Title/Message + createMessageSection(); + + // 3. Separator + Label separator = new Label(mainComposite, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + // 4. Action Buttons + createActionButtonsSection(); + + parent.layout(); + + // Add mouse tracking to keep popup open when hovering + addMouseTrackingToAllChildren(mainComposite); + addMouseTrackingToAllChildren(parent); + + System.out.println("[FINDINGS-HOVER] ✓ Content created successfully"); + + } catch (Exception e) { + System.err.println("[FINDINGS-HOVER] Error creating content: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void addMouseTrackingToAllChildren(Composite composite) { + if (composite == null || composite.isDisposed()) return; + + composite.addMouseTrackListener(new org.eclipse.swt.events.MouseTrackListener() { + @Override + public void mouseEnter(org.eclipse.swt.events.MouseEvent e) { + System.out.println("[FINDINGS-HOVER] Mouse ENTERED control"); + if (closeTimer != null) { + closeTimer.cancel(); + closeTimer = null; + } + } + + @Override + public void mouseExit(org.eclipse.swt.events.MouseEvent e) { + System.out.println("[FINDINGS-HOVER] Mouse EXITED control - starting close timer"); + if (closeTimer != null) { + closeTimer.cancel(); + } + closeTimer = new Timer(); + closeTimer.schedule(new TimerTask() { + @Override + public void run() { + Display.getDefault().asyncExec(() -> { + try { + CxFindingsHoverControl.super.dispose(); + } catch (Exception ex) { + // Already disposed + } + }); + } + }, 500); + } + + @Override + public void mouseHover(org.eclipse.swt.events.MouseEvent e) { + // Not needed + } + }); + + // Recursively add to all children + for (org.eclipse.swt.widgets.Control child : composite.getChildren()) { + if (child instanceof Composite) { + addMouseTrackingToAllChildren((Composite) child); + } + } + } + + /** + * Create severity badge with color + */ + private void createSeverityBadge() { + Composite severityComposite = new Composite(mainComposite, SWT.NONE); + severityComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + GridLayout severityLayout = new GridLayout(2, false); + severityLayout.marginHeight = 6; + severityLayout.marginWidth = 8; + severityLayout.verticalSpacing = 0; + severityLayout.horizontalSpacing = 8; + severityComposite.setLayout(severityLayout); + severityComposite.setBackground(getSeverityColor()); + + Label severityIcon = new Label(severityComposite, SWT.NONE); + severityIcon.setText(getSeverityIcon()); + severityIcon.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); + severityIcon.setFont(mainComposite.getFont()); + + Label severityLabel = new Label(severityComposite, SWT.NONE); + severityLabel.setText("CHECKMARX FINDING"); + severityLabel.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); + severityLabel.setFont(mainComposite.getFont()); + } + + /** + * Create message/title section + */ + private void createMessageSection() { + Label messageLabel = new Label(mainComposite, SWT.WRAP); + messageLabel.setText(annotation.getTitle()); + messageLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + messageLabel.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); + + // ✓ Use Browser for HTML descriptions (with buttons) + if (annotation.getDescription() != null && !annotation.getDescription().isEmpty()) { + org.eclipse.swt.browser.Browser browser = new org.eclipse.swt.browser.Browser(mainComposite, SWT.NONE); + browser.setText(annotation.getDescription()); // ← Now renders HTML properly + GridData browserData = new GridData(SWT.FILL, SWT.FILL, true, true); + browserData.widthHint = 400; + browserData.heightHint = 120; + browser.setLayoutData(browserData); + } + } + + /** + * Create action buttons + */ + private void createActionButtonsSection() { + Composite buttonComposite = new Composite(mainComposite, SWT.NONE); + buttonComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + GridLayout buttonLayout = new GridLayout(2, true); + buttonLayout.marginHeight = 4; + buttonLayout.marginWidth = 0; + buttonLayout.horizontalSpacing = 6; + buttonComposite.setLayout(buttonLayout); + + // Ignore Button + Button ignoreBtn = new Button(buttonComposite, 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) { + System.out.println("[FINDINGS-HOVER] Ignore clicked for: " + annotation.getTitle()); + dispose(); + } + }); + + // Details Button + Button detailsBtn = new Button(buttonComposite, SWT.PUSH); + detailsBtn.setText("Details"); + detailsBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + detailsBtn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + System.out.println("[FINDINGS-HOVER] Details clicked for: " + annotation.getTitle()); + dispose(); + } + }); + } + + /** + * Get severity color from annotation type + */ + private Color getSeverityColor() { + Display display = Display.getCurrent(); + String annotationType = annotation.getType(); + + if (annotationType != null) { + if (annotationType.contains("critical")) { + return display.getSystemColor(SWT.COLOR_RED); + } else if (annotationType.contains("high")) { + return display.getSystemColor(SWT.COLOR_DARK_RED); + } else if (annotationType.contains("medium")) { + return display.getSystemColor(SWT.COLOR_DARK_YELLOW); + } + } + + return display.getSystemColor(SWT.COLOR_DARK_BLUE); + } + + /** + * Get severity icon emoji + */ + private String getSeverityIcon() { + String annotationType = annotation.getType(); + if (annotationType != null) { + if (annotationType.contains("critical")) { + return "🔴"; + } else if (annotationType.contains("high")) { + return "🟠"; + } else if (annotationType.contains("medium")) { + return "🟡"; + } + } + return "🔵"; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java new file mode 100644 index 00000000..4a2c2dae --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java @@ -0,0 +1,47 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.AbstractInformationControl; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.*; + +public class CxFindingsInformationControl extends AbstractInformationControl { + + public CxFindingsInformationControl(Shell parentShell) { + super(parentShell, false); + create(); + } + + @Override + protected void createContent(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(new GridLayout(4, true)); + + // Label + Label title = new Label(composite, SWT.NONE); + title.setText("Checkmarx Finding Detected"); + GridData gd = new GridData(SWT.FILL, SWT.CENTER, true, false, 4, 1); + title.setLayoutData(gd); + + // Clickable SWT Buttons inside Hover Popup + Button quickFixBtn = new Button(composite, SWT.PUSH); + quickFixBtn.setText("⚡ Quick Fix"); + quickFixBtn.addListener(SWT.Selection, e -> System.out.println("Quick Fix clicked from hover!")); + + Button ignoreBtn = new Button(composite, SWT.PUSH); + ignoreBtn.setText("🚫 Ignore"); + ignoreBtn.addListener(SWT.Selection, e -> System.out.println("Ignore clicked from hover!")); + + Button copyBtn = new Button(composite, SWT.PUSH); + copyBtn.setText("📋 Copy"); + + Button detailsBtn = new Button(composite, SWT.PUSH); + detailsBtn.setText("🪟 Details"); + } + + @Override + public boolean hasContents() { + return true; + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java new file mode 100644 index 00000000..b6fa460c --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java @@ -0,0 +1,71 @@ +//package com.checkmarx.eclipse.devassist.ui.findings.editor; +// +//import org.eclipse.jface.text.source.Annotation; +// +///** +// * Custom annotation for Findings window highlighting. +// * Represents a security finding annotation in the editor with title and description. +// */ +//public class FindingsAnnotation extends Annotation { +// +// private final String title; +// private final String description; +// +// public FindingsAnnotation(String annotationType, String title, String description) { +// super(annotationType, false, null); +// this.title = title; +// this.description = description; +// } +// +// public String getTitle() { +// return title; +// } +// +// public String getDescription() { +// return description; +// } +// +// @Override +// public String getText() { +// return title; +// } +//} +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.jface.text.source.Annotation; + +public class FindingsAnnotation extends Annotation { + + private String title; + private String description; + private List buttons = new ArrayList<>(); + + public FindingsAnnotation(String type, String title, String description) { + super(type, false, title); + this.title = title; + this.description = description; + } + + public void addButton(String label, Runnable action) { + buttons.add(new AnnotationButton(label, action)); + } + + public List getButtons() { + return buttons; + } + + public String getTitle() { return title; } + public String getDescription() { return description; } + + public static class AnnotationButton { + public String label; + public Runnable action; + + public AnnotationButton(String label, Runnable action) { + this.label = label; + this.action = action; + } + } +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java new file mode 100644 index 00000000..5004d8da --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java @@ -0,0 +1,136 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.ui.editors.text.TextEditor; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Manages highlighting and underlining of problematic code lines in the editor. + * Provides visual feedback for findings by underlining vulnerable code with severity-based colors. + * + * Supports: + * - Red wavy underline for CRITICAL/HIGH issues + * - Yellow wavy underline for MEDIUM issues + * - Blue wavy underline for LOW issues + * - Auto-clear on navigation away + */ +public class FindingsEditorOverlay { + + // These match the annotation types defined in plugin.xml + private static final String ANNOTATION_TYPE_CRITICAL = "com.checkmarx.eclipse.findings.critical"; + private static final String ANNOTATION_TYPE_HIGH = "com.checkmarx.eclipse.findings.high"; + private static final String ANNOTATION_TYPE_MEDIUM = "com.checkmarx.eclipse.findings.medium"; + private static final String ANNOTATION_TYPE_LOW = "com.checkmarx.eclipse.findings.low"; + + /** + * Highlight a problematic line in the editor. + * + * @param editor The TextEditor to highlight in + * @param issue The scan issue containing location information + */ + public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { + try { + if (editor == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { + return; + } + + Location location = issue.getLocations().get(0); + int lineNumber = location.getLine() - 1; // Convert to 0-based + + ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); + if (viewer == null) { + System.out.println("[FINDINGS-OVERLAY] Could not get source viewer from editor"); + return; + } + + IDocument document = viewer.getDocument(); + if (document == null || lineNumber < 0 || lineNumber >= document.getNumberOfLines()) { + System.out.println("[FINDINGS-OVERLAY] Invalid document or line number: " + lineNumber); + return; + } + + // Get line start and end offsets + int lineStartOffset = document.getLineOffset(lineNumber); + int lineLength = document.getLineLength(lineNumber); + int lineEndOffset = lineStartOffset + lineLength; + + // Create annotation for the line + String annotationType = getAnnotationTypeForSeverity(issue.getSeverity()); + FindingsAnnotation annotation = new FindingsAnnotation(annotationType, issue.getTitle(), issue.getDescription()); + Position position = new Position(lineStartOffset, lineEndOffset - lineStartOffset); + + // Add annotation to model + IAnnotationModel annotationModel = viewer.getAnnotationModel(); + if (annotationModel != null) { + annotationModel.addAnnotation(annotation, position); + System.out.println("Annotation added"); + System.out.println("Annotation model = " + annotationModel.getClass().getName()); + System.out.println("Annotation type = " + annotation.getType()); + System.out.println("Offset = " + position.offset); + System.out.println("Length = " + position.length); + } + } catch (BadLocationException e) { + System.out.println("[FINDINGS-OVERLAY] Error highlighting line: " + e.getMessage()); + } + } + + /** + * Clear all findings annotations from the editor. + */ + public static void clearHighlights(TextEditor editor) { + try { + if (editor == null) { + return; + } + + ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); + if (viewer == null) { + return; + } + + IAnnotationModel annotationModel = viewer.getAnnotationModel(); + if (annotationModel == null) { + return; + } + + // Remove all findings annotations + annotationModel.getAnnotationIterator().forEachRemaining(annotation -> { + if (annotation instanceof FindingsAnnotation) { + annotationModel.removeAnnotation(annotation); + } + }); + + System.out.println("[FINDINGS-OVERLAY] ✓ Cleared all findings highlights"); + } catch (Exception e) { + System.out.println("[FINDINGS-OVERLAY] Error clearing highlights: " + e.getMessage()); + } + } + + /** + * Get annotation type based on severity level. + */ + private static String getAnnotationTypeForSeverity(String severity) { + if (severity == null) { + return ANNOTATION_TYPE_MEDIUM; + } + + switch (severity.toLowerCase()) { + case "critical": + case "high": + return ANNOTATION_TYPE_CRITICAL; + case "medium": + return ANNOTATION_TYPE_MEDIUM; + case "low": + case "info": + return ANNOTATION_TYPE_LOW; + default: + return ANNOTATION_TYPE_MEDIUM; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java new file mode 100644 index 00000000..111c1cdd --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java @@ -0,0 +1,321 @@ +package com.checkmarx.eclipse.devassist.ui.findings.editor; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IInformationControl; +import org.eclipse.jface.text.IInformationControlCreator; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextHover; +import org.eclipse.jface.text.ITextHoverExtension; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Text; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Provides custom hover information for findings in the editor. + * Shows issue details and action buttons on hover. + */ +public class FindingsHoverProvider implements ITextHover, ITextHoverExtension { + + private ScanIssue currentIssue; + private ITextViewer textViewer; + + public FindingsHoverProvider(ScanIssue issue, ITextViewer viewer) { + this.currentIssue = issue; + this.textViewer = viewer; + } + + @Override + public String getHoverInfo(ITextViewer viewer, IRegion hoverRegion) { + System.out.println("[HOVER] getHoverInfo()"); + if (currentIssue == null) { + return null; + } + + // Build tooltip text (used as cache key by the hover framework; actual UI comes from + // FindingsInformationControl, built from currentIssue directly). + StringBuilder tooltip = new StringBuilder(); + String severity = currentIssue.getSeverity(); + tooltip.append("=== ").append(severity != null ? severity.toUpperCase() : "UNKNOWN").append(" ===\n\n"); + tooltip.append("Title: ").append(currentIssue.getTitle()).append("\n\n"); + tooltip.append("Description:\n").append(currentIssue.getDescription()).append("\n\n"); + if (currentIssue.getRemediationAdvise() != null) { + tooltip.append("Remediation:\n").append(currentIssue.getRemediationAdvise()).append("\n\n"); + } + tooltip.append("[This is a custom hover with action buttons below]\n"); + tooltip.append(" [Quick Fix] [Ignore] [Copy] [Open Details]"); + + return tooltip.toString(); + } + + @Override + public IRegion getHoverRegion(ITextViewer viewer, int offset) { + + System.out.println("[HOVER] getHoverRegion offset=" + offset); + // Return region covering the entire line if it's the problematic line + try { + if (viewer == null || currentIssue == null + || currentIssue.getLocations() == null || currentIssue.getLocations().isEmpty()) { + return null; + } + + IDocument document = viewer.getDocument(); + if (document == null) { + return null; + } + + int lineNumber = document.getLineOfOffset(offset); + int problematicLine = currentIssue.getLocations().get(0).getLine() - 1; + if (lineNumber == problematicLine) { + int lineStartOffset = document.getLineOffset(lineNumber); + int lineLength = document.getLineLength(lineNumber); + return new Region(lineStartOffset, lineLength); + } + } catch (BadLocationException e) { + // Offset no longer valid (e.g. document changed) - no hover to show + } + return null; + } + + @Override + public IInformationControlCreator getHoverControlCreator() { + return new IInformationControlCreator() { + @Override + public IInformationControl createInformationControl(Shell parent) { + return new FindingsInformationControl(parent, currentIssue); + } + }; + } + + /** + * Custom information control for displaying findings with action buttons. + */ + public static class FindingsInformationControl implements IInformationControl { + + private Shell shell; + private ScanIssue issue; + + public FindingsInformationControl(Shell parent, ScanIssue issue) { + this.issue = issue; + this.shell = new Shell(parent, SWT.TOOL | SWT.ON_TOP); + this.shell.setLayout(new GridLayout(1, false)); + this.shell.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + + createContents(); + } + + private void createContents() { + // Severity label + Label severityLabel = new Label(shell, SWT.NONE); + severityLabel.setText(getSeverityIcon(issue.getSeverity()) + " " + getSeverityText(issue.getSeverity())); + severityLabel.setFont(shell.getDisplay().getSystemFont()); + GridData gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + severityLabel.setLayoutData(gd); + + // Title label + Label titleLabel = new Label(shell, SWT.WRAP); + titleLabel.setText("Title: " + (issue.getTitle() != null ? issue.getTitle() : "")); + gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + gd.widthHint = 400; + titleLabel.setLayoutData(gd); + + // Description text (scrollable) + Text descriptionText = new Text(shell, SWT.WRAP | SWT.READ_ONLY | SWT.V_SCROLL); + descriptionText.setText(issue.getDescription() != null ? issue.getDescription() : ""); + gd = new GridData(SWT.FILL, SWT.FILL, true, true); + gd.heightHint = 90; + gd.widthHint = 400; + descriptionText.setLayoutData(gd); + + // Remediation advice (if available) + if (issue.getRemediationAdvise() != null && !issue.getRemediationAdvise().isEmpty()) { + Label remediationLabel = new Label(shell, SWT.WRAP); + remediationLabel.setText("Remediation: " + issue.getRemediationAdvise()); + gd = new GridData(SWT.FILL, SWT.CENTER, true, false); + gd.widthHint = 400; + remediationLabel.setLayoutData(gd); + } + + // Buttons composite + Composite buttonsComposite = new Composite(shell, 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.addListener(SWT.Selection, 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.addListener(SWT.Selection, 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.addListener(SWT.Selection, e -> onCopyClick()); + + // Open Details button + Button openBtn = new Button(buttonsComposite, SWT.PUSH); + openBtn.setText("Open Window"); + openBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + openBtn.addListener(SWT.Selection, e -> onOpenWindowClick()); + } + + private static String getSeverityIcon(String severity) { + if (severity == null) { + return "⚪"; // white circle for unknown + } + switch (severity.toLowerCase()) { + case "critical": + return "🔴"; + case "high": + return "🟠"; + case "medium": + return "🟡"; + case "low": + return "🟢"; + default: + return "⚪"; + } + } + + private static String getSeverityText(String severity) { + return severity != null ? severity.toUpperCase() : "UNKNOWN"; + } + + private void onQuickFixClick() { + System.out.println("[FINDINGS-HOVER] Quick Fix clicked for: " + issue.getTitle()); + // TODO: Implement quick fix logic + } + + private void onIgnoreClick() { + System.out.println("[FINDINGS-HOVER] Ignore clicked for: " + issue.getTitle()); + // 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; + shell.getDisplay().asyncExec(() -> { + org.eclipse.swt.dnd.Clipboard clipboard = new org.eclipse.swt.dnd.Clipboard(shell.getDisplay()); + org.eclipse.swt.dnd.TextTransfer transfer = org.eclipse.swt.dnd.TextTransfer.getInstance(); + clipboard.setContents(new Object[] { text }, new org.eclipse.swt.dnd.Transfer[] { transfer }); + clipboard.dispose(); + System.out.println("[FINDINGS-HOVER] ✓ Copied to clipboard"); + }); + } + + private void onOpenWindowClick() { + System.out.println("[FINDINGS-HOVER] Open Window clicked for: " + issue.getTitle()); + // TODO: Open Findings window and navigate to issue + } + + @Override + public void setInformation(String information) { + } + + @Override + public void setSize(int width, int height) { + if (shell != null) { + shell.setSize(width, height); + } + } + + @Override + public void setLocation(Point location) { + if (shell != null && location != null) { + shell.setLocation(location); + } + } + + @Override + public void setSizeConstraints(int maxWidth, int maxHeight) { + } + + @Override + public void dispose() { + if (shell != null && !shell.isDisposed()) { + shell.dispose(); + } + } + + @Override + public void setVisible(boolean visible) { + if (shell != null) { + shell.setVisible(visible); + } + } + + @Override + public void setForegroundColor(org.eclipse.swt.graphics.Color color) { + } + + @Override + public void setBackgroundColor(org.eclipse.swt.graphics.Color color) { + } + + @Override + public boolean isFocusControl() { + return shell != null && shell.isFocusControl(); + } + + @Override + public void setFocus() { + if (shell != null) { + shell.setFocus(); + } + } + + @Override + public void addDisposeListener(org.eclipse.swt.events.DisposeListener listener) { + if (shell != null) { + shell.addDisposeListener(listener); + } + } + + @Override + public void removeDisposeListener(org.eclipse.swt.events.DisposeListener listener) { + if (shell != null) { + shell.removeDisposeListener(listener); + } + } + + @Override + public Point computeSizeHint() { + return new Point(400, 200); + } + + @Override + public void addFocusListener(FocusListener listener) { + if (shell != null) { + shell.addFocusListener(listener); + } + } + + @Override + public void removeFocusListener(FocusListener listener) { + if (shell != null) { + shell.removeFocusListener(listener); + } + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java new file mode 100644 index 00000000..aab54039 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java @@ -0,0 +1,136 @@ +package com.checkmarx.eclipse.devassist.ui.findings.icons; + +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.plugin.AbstractUIPlugin; + +import com.checkmarx.eclipse.Activator; + +/** + * Registry for managing Checkmarx severity icons. + * Handles icon loading and caching for different sizes and themes. + */ +public class IconRegistry { + + public enum Size { + SMALL("_16"), + MEDIUM("_20"); + + private final String suffix; + + Size(String suffix) { + this.suffix = suffix; + } + + public String getSuffix() { + return suffix; + } + } + + public enum Severity { + MALICIOUS("malicious"), + CRITICAL("critical"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"), + UNKNOWN("unknown"), + OK("ok"), + IGNORED("ignored"); + + private final String name; + + Severity(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + private static ImageRegistry imageRegistry; + + static { + initializeRegistry(); + } + + private static void initializeRegistry() { + imageRegistry = PlatformUI.getWorkbench().getDisplay() != null + ? new ImageRegistry(PlatformUI.getWorkbench().getDisplay()) + : new ImageRegistry(); + + // Register small icons (16px) + registerIcon("malicious_16", "icons/severity/malicious_16.svg"); + registerIcon("critical_16", "icons/severity/critical_16.svg"); + registerIcon("high_16", "icons/severity/high_16.svg"); + registerIcon("medium_16", "icons/severity/medium_16.svg"); + registerIcon("low_16", "icons/severity/low_16.svg"); + registerIcon("unknown_16", "icons/severity/unknown_16.svg"); + registerIcon("ok_16", "icons/severity/ok_16.svg"); + registerIcon("ignored_16", "icons/severity/ignored_16.svg"); + + // Register medium icons (20px) + registerIcon("malicious_20", "icons/severity/malicious_20.svg"); + registerIcon("critical_20", "icons/severity/critical_20.svg"); + registerIcon("high_20", "icons/severity/high_20.svg"); + registerIcon("medium_20", "icons/severity/medium_20.svg"); + registerIcon("low_20", "icons/severity/low_20.svg"); + registerIcon("unknown_20", "icons/severity/unknown_20.svg"); + registerIcon("ok_20", "icons/severity/ok_20.svg"); + registerIcon("ignored_20", "icons/severity/ignored_20.svg"); + + // Register base icons + registerIcon("malicious", "icons/severity/malicious.svg"); + registerIcon("critical", "icons/severity/critical.svg"); + registerIcon("high", "icons/severity/high.svg"); + registerIcon("medium", "icons/severity/medium.svg"); + registerIcon("low", "icons/severity/low.svg"); + registerIcon("unknown", "icons/severity/unknown.svg"); + registerIcon("ok", "icons/severity/ok.svg"); + registerIcon("ignored", "icons/severity/ignored.svg"); + } + + private static void registerIcon(String key, String path) { + AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, path); + imageRegistry.put(key, AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, path)); + } + + /** + * Get icon for a severity level and size. + * Normalizes severity to match SeverityLevel enum values, then converts to lowercase for icon lookup. + * + * @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; + } + + // Normalize severity to SeverityLevel format, then convert to lowercase for icon key + String normalized = com.checkmarx.eclipse.devassist.backend.DevAssistUtils.normalizeSeverity(severity); + String key = normalized.toLowerCase() + size.getSuffix(); + return imageRegistry.get(key); + } + + /** + * Get icon for a severity level with default small size. + * + * @param severity Severity level + * @return Image instance or null if not found + */ + public static Image getIcon(String severity) { + return getIcon(severity, Size.SMALL); + } + + /** + * Get image registry. + * + * @return ImageRegistry instance + */ + public static ImageRegistry getRegistry() { + return imageRegistry; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java new file mode 100644 index 00000000..558f67cd --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java @@ -0,0 +1,255 @@ +package com.checkmarx.eclipse.devassist.ui.findings.icons; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import java.util.HashMap; +import java.util.Map; + +/** + * Composes severity icons into a single visual representation. + * Creates badges like: [C:4] [H:3] [M:1] as actual icon images + */ +public class SeverityImageComposer { + + private static final Map compositeImageCache = new HashMap<>(); + + /** + * Create a full composite image with severity icon badges displayed inline. + * Shows actual colored severity icons (🔴 🟠 🟡 🟢) after the filename. + */ + public static Image createFullCompositeImage(FileNodeLabel fileNode) { + if (fileNode == null || fileNode.getProblemCount() == null || fileNode.getProblemCount().isEmpty()) { + return null; + } + + try { + Display display = Display.getDefault(); + Image compositeImage = createFullBadgeImage(display, fileNode); + if (compositeImage != null) { + System.out.println("[SEVERITY-COMPOSER] Created full composite image with severity icons"); + } + return compositeImage; + } catch (Exception e) { + System.err.println("[SEVERITY-COMPOSER] Error creating full composite image: " + e.getMessage()); + e.printStackTrace(); + 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) { + System.err.println("[SEVERITY-COMPOSER] Error creating composite image: " + e.getMessage()); + return null; + } + } + + /** + * Create a badge image showing severity levels inline + */ + private static Image createBadgeImage(Display display, FileNodeLabel fileNode) { + try { + // Get individual severity icons + Image criticalIcon = IconRegistry.getIcon("critical", IconRegistry.Size.SMALL); // 16x16 + Image highIcon = IconRegistry.getIcon("high", IconRegistry.Size.SMALL); + Image mediumIcon = IconRegistry.getIcon("medium", IconRegistry.Size.SMALL); + Image lowIcon = IconRegistry.getIcon("low", IconRegistry.Size.SMALL); + + // Calculate total width needed + int iconSize = 16; + int spacing = 1; + int width = 0; + + if (hasCount(fileNode, "critical")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "high")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "medium")) { + width += iconSize + spacing; + } + if (hasCount(fileNode, "low")) { + width += iconSize + spacing; + } + + if (width == 0) { + return null; + } + + // Adjust width to remove last spacing + width = Math.max(0, width - spacing); + + // Create composite image + Image compositeImage = new Image(display, width, iconSize); + GC gc = new GC(compositeImage); + gc.setBackground(display.getSystemColor(org.eclipse.swt.SWT.COLOR_WIDGET_BACKGROUND)); + gc.fillRectangle(0, 0, width, iconSize); + gc.setAntialias(org.eclipse.swt.SWT.ON); + + int x = 0; + int y = 0; + + // Draw critical icon if count > 0 + if (hasCount(fileNode, "critical") && criticalIcon != null) { + gc.drawImage(criticalIcon, x, y); + x += iconSize + spacing; + } + + // Draw high icon if count > 0 + if (hasCount(fileNode, "high") && highIcon != null) { + gc.drawImage(highIcon, x, y); + x += iconSize + spacing; + } + + // Draw medium icon if count > 0 + if (hasCount(fileNode, "medium") && mediumIcon != null) { + gc.drawImage(mediumIcon, x, y); + x += iconSize + spacing; + } + + // Draw low icon if count > 0 + if (hasCount(fileNode, "low") && lowIcon != null) { + gc.drawImage(lowIcon, x, y); + x += iconSize + spacing; + } + + gc.dispose(); + System.out.println("[SEVERITY-COMPOSER] Created composite image: " + width + "x" + iconSize); + return compositeImage; + + } catch (Exception e) { + System.err.println("[SEVERITY-COMPOSER] Error creating badge image: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * Create a full badge image showing only severity icons inline (no text). + * Displays: [🔴][🟠][🟡][🟢] based on which severities have counts + */ + private static Image createFullBadgeImage(Display display, FileNodeLabel fileNode) { + try { + // Get individual severity icons + Image criticalIcon = IconRegistry.getIcon("critical", IconRegistry.Size.SMALL); // 16x16 + Image highIcon = IconRegistry.getIcon("high", IconRegistry.Size.SMALL); + Image mediumIcon = IconRegistry.getIcon("medium", IconRegistry.Size.SMALL); + Image lowIcon = IconRegistry.getIcon("low", IconRegistry.Size.SMALL); + + // Calculate total width needed + int iconSize = 16; + int spacing = 2; + int totalWidth = 0; + + // Count how many icons we need + int iconCount = 0; + if (hasCount(fileNode, "critical")) iconCount++; + if (hasCount(fileNode, "high")) iconCount++; + if (hasCount(fileNode, "medium")) iconCount++; + if (hasCount(fileNode, "low")) iconCount++; + + if (iconCount == 0) { + return null; + } + + // Calculate width: (iconSize + spacing) * count - spacing + totalWidth = (iconSize + spacing) * iconCount - spacing; + + // Create composite image with severity icons + Image compositeImage = new Image(display, totalWidth, iconSize); + GC gc = new GC(compositeImage); + gc.setBackground(display.getSystemColor(org.eclipse.swt.SWT.COLOR_WIDGET_BACKGROUND)); + gc.fillRectangle(0, 0, totalWidth, iconSize); + gc.setAntialias(org.eclipse.swt.SWT.ON); + + int x = 0; + int y = 0; + + // Draw critical icon + if (hasCount(fileNode, "critical") && criticalIcon != null) { + gc.drawImage(criticalIcon, x, y); + x += iconSize + spacing; + } + + // Draw high icon + if (hasCount(fileNode, "high") && highIcon != null) { + gc.drawImage(highIcon, x, y); + x += iconSize + spacing; + } + + // Draw medium icon + if (hasCount(fileNode, "medium") && mediumIcon != null) { + gc.drawImage(mediumIcon, x, y); + x += iconSize + spacing; + } + + // Draw low icon + if (hasCount(fileNode, "low") && lowIcon != null) { + gc.drawImage(lowIcon, x, y); + x += iconSize + spacing; + } + + gc.dispose(); + System.out.println("[SEVERITY-COMPOSER] Created full badge image: " + totalWidth + "x" + iconSize); + return compositeImage; + + } catch (Exception e) { + System.err.println("[SEVERITY-COMPOSER] Error creating full badge image: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + private static boolean hasCount(FileNodeLabel fileNode, String severity) { + Long count = fileNode.getProblemCount().get(severity); + return count != null && count > 0; + } + + private static String createCacheKey(FileNodeLabel fileNode) { + StringBuilder key = new StringBuilder(); + key.append("c:").append(fileNode.getProblemCount().getOrDefault("critical", 0L)).append("|"); + key.append("h:").append(fileNode.getProblemCount().getOrDefault("high", 0L)).append("|"); + key.append("m:").append(fileNode.getProblemCount().getOrDefault("medium", 0L)).append("|"); + key.append("l:").append(fileNode.getProblemCount().getOrDefault("low", 0L)); + return key.toString(); + } + + public static void clearCache() { + for (Image img : compositeImageCache.values()) { + if (img != null && !img.isDisposed()) { + img.dispose(); + } + } + compositeImageCache.clear(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java new file mode 100644 index 00000000..88ff5aa2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java @@ -0,0 +1,269 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignored; + +import java.util.List; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.ui.part.ViewPart; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore.IgnoredProblemsListener; + +/** + * Custom view for displaying ignored problems/findings. Shows problems that have been + * explicitly ignored from the native Problems View and findings from the Findings View. + * Allows restoring problems/findings back to the active list. + */ +public class CxIgnoredProblemsView extends ViewPart implements IgnoredProblemsListener { + + public static final String ID = "com.checkmarx.eclipse.devassist.ui.findings.ignored.CxIgnoredProblemsView"; + + private TreeViewer treeViewer; + private IgnoredProblemsStore ignoredStore; + private List allIssues; + + @Override + public void createPartControl(Composite parent) { + System.out.println("[IGNORED-VIEW] Creating Ignored Problems View..."); + + ignoredStore = IgnoredProblemsStore.getInstance(); + ignoredStore.addListener(this); + + // Create TreeViewer + treeViewer = new TreeViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER); + treeViewer.setContentProvider(new IgnoredProblemsContentProvider()); + treeViewer.setLabelProvider(new IgnoredProblemsLabelProvider()); + treeViewer.setInput(new java.util.ArrayList<>()); + + // Setup toolbar + setupToolbar(); + + // Setup context menu + setupContextMenu(); + + // Setup double-click listener + treeViewer.addDoubleClickListener(event -> { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + Object selected = ((IStructuredSelection) selection).getFirstElement(); + if (selected instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) selected; + navigateToIgnoredIssue(issue); + } + } + }); + + System.out.println("[IGNORED-VIEW] ✓ Ignored Problems View created"); + } + + private void setupToolbar() { + IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); + + Action restoreAllAction = new Action("Restore All Ignored Problems") { + @Override + public void run() { + System.out.println("[IGNORED-VIEW] Restoring all ignored problems..."); + ignoredStore.clearAll(); + refreshView(); + } + }; + restoreAllAction.setToolTipText("Restore all ignored problems to active findings"); + toolbarManager.add(restoreAllAction); + + Action clearAllAction = new Action("Clear Ignored List") { + @Override + public void run() { + System.out.println("[IGNORED-VIEW] Clearing all ignored problems permanently..."); + ignoredStore.clearAll(); + refreshView(); + } + }; + clearAllAction.setToolTipText("Permanently clear the ignored problems list"); + toolbarManager.add(clearAllAction); + } + + private void setupContextMenu() { + Menu contextMenu = new Menu(treeViewer.getTree()); + treeViewer.getTree().setMenu(contextMenu); + + MenuItem restoreItem = new MenuItem(contextMenu, SWT.PUSH); + restoreItem.setText("Restore This Finding"); + restoreItem.addListener(SWT.Selection, event -> { + IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); + if (selection.getFirstElement() instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) selection.getFirstElement(); + System.out.println("[IGNORED-VIEW] Restoring finding: " + issue.getScanIssueId()); + ignoredStore.restoreProblem(issue.getScanIssueId()); + refreshView(); + } + }); + + new MenuItem(contextMenu, SWT.SEPARATOR); + + MenuItem navigateItem = new MenuItem(contextMenu, SWT.PUSH); + navigateItem.setText("Go to Line"); + navigateItem.addListener(SWT.Selection, event -> { + IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); + if (selection.getFirstElement() instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) selection.getFirstElement(); + navigateToIgnoredIssue(issue); + } + }); + + new MenuItem(contextMenu, SWT.SEPARATOR); + + MenuItem deleteItem = new MenuItem(contextMenu, SWT.PUSH); + deleteItem.setText("Delete from Ignore List"); + deleteItem.addListener(SWT.Selection, event -> { + IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); + if (selection.getFirstElement() instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) selection.getFirstElement(); + System.out.println("[IGNORED-VIEW] Permanently removing from ignore list: " + issue.getScanIssueId()); + ignoredStore.restoreProblem(issue.getScanIssueId()); + refreshView(); + } + }); + } + + private void navigateToIgnoredIssue(ScanIssue issue) { + if (issue != null && issue.getFilePath() != null) { + int lineNumber = (issue.getLocations() != null && !issue.getLocations().isEmpty()) + ? issue.getLocations().get(0).getLine() : 0; + System.out.println("[IGNORED-VIEW] Navigating to: " + issue.getFilePath() + " line " + lineNumber); + org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { + try { + org.eclipse.core.resources.IWorkspaceRoot root = org.eclipse.core.resources.ResourcesPlugin + .getWorkspace().getRoot(); + // Find file in workspace by simple name + String simpleName = new org.eclipse.core.runtime.Path(issue.getFilePath()).lastSegment(); + final org.eclipse.core.resources.IFile[] found = new org.eclipse.core.resources.IFile[1]; + + root.accept(proxy -> { + if (proxy.getType() == org.eclipse.core.resources.IResource.FILE && + proxy.getName().equals(simpleName)) { + found[0] = (org.eclipse.core.resources.IFile) proxy.requestResource(); + return false; + } + return true; + }, org.eclipse.core.resources.IResource.NONE); + + if (found[0] != null) { + org.eclipse.ui.IWorkbenchWindow window = org.eclipse.ui.PlatformUI.getWorkbench() + .getActiveWorkbenchWindow(); + if (window != null) { + org.eclipse.ui.IWorkbenchPage page = window.getActivePage(); + if (page != null) { + // Position cursor at the issue line using temporary marker + if (lineNumber > 0) { + positionCursorAtLineWithMarker(page, found[0], lineNumber); + } else { + // No line info, just open the file + org.eclipse.ui.ide.IDE.openEditor(page, found[0], true); + System.out.println("[IGNORED-VIEW] ✓ Opened file: " + simpleName); + } + } + } + } else { + System.out.println("[IGNORED-VIEW] File not found in workspace: " + simpleName); + } + } catch (Exception e) { + System.err.println("[IGNORED-VIEW] Error navigating: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + } + + /** + * Position cursor using temporary marker - same approach as native Problems View. + * This is the most reliable method that works with all Eclipse editors. + */ + private void positionCursorAtLineWithMarker(org.eclipse.ui.IWorkbenchPage page, + org.eclipse.core.resources.IFile file, int lineNumber) { + try { + // Create temporary marker with line number + org.eclipse.core.resources.IMarker tempMarker = file.createMarker("org.eclipse.core.resources.textmarker"); + tempMarker.setAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, lineNumber); + tempMarker.setAttribute(org.eclipse.core.resources.IMarker.TRANSIENT, true); + + // Open editor + org.eclipse.ui.IEditorPart editor = org.eclipse.ui.ide.IDE.openEditor(page, file, true); + System.out.println("[IGNORED-VIEW] ✓ Opened file: " + file.getName()); + + // Use IDE.gotoMarker to position cursor (same as native Problems View) + if (editor != null) { + org.eclipse.ui.ide.IDE.gotoMarker(editor, tempMarker); + System.out.println("[IGNORED-VIEW] ✓ Cursor positioned at line " + lineNumber); + } + + // Delete the temporary marker + try { + tempMarker.delete(); + } catch (Exception e) { + System.out.println("[IGNORED-VIEW] Could not delete temporary marker: " + e.getMessage()); + } + } catch (Exception e) { + System.err.println("[IGNORED-VIEW] Error positioning cursor: " + e.getMessage()); + // Fallback: just open the file + try { + org.eclipse.ui.ide.IDE.openEditor(page, file, true); + System.out.println("[IGNORED-VIEW] ✓ Opened file (fallback): " + file.getName()); + } catch (Exception fallbackEx) { + System.err.println("[IGNORED-VIEW] Error in fallback: " + fallbackEx.getMessage()); + } + } + } + + @Override + public void onIgnoredProblemsChanged() { + System.out.println("[IGNORED-VIEW] Ignored problems changed, refreshing view..."); + refreshView(); + } + + private void refreshView() { + if (treeViewer != null && !treeViewer.getTree().isDisposed()) { + try { + // Get all ignored issues including cached findings from the Findings View + List ignoredIssues = ignoredStore.getAllIgnoredProblems(allIssues); + + System.out.println("[IGNORED-VIEW] Displaying " + ignoredIssues.size() + + " ignored findings (Total known: " + (allIssues != null ? allIssues.size() : 0) + ")"); + + treeViewer.setInput(ignoredIssues); + treeViewer.expandAll(); + } catch (Exception e) { + System.err.println("[IGNORED-VIEW] Error refreshing view: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * Update the view with all issues. + */ + public void updateIssues(List issues) { + this.allIssues = issues; + refreshView(); + } + + @Override + public void setFocus() { + if (treeViewer != null) { + treeViewer.getTree().setFocus(); + } + } + + @Override + public void dispose() { + ignoredStore.removeListener(this); + super.dispose(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java new file mode 100644 index 00000000..f4f2a9c4 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java @@ -0,0 +1,78 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignored; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Content provider for ignored problems tree view. Organizes issues by file. + */ +public class IgnoredProblemsContentProvider implements ITreeContentProvider { + + private Map> fileToIssues = new HashMap<>(); + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + fileToIssues.clear(); + if (newInput instanceof List) { + @SuppressWarnings("unchecked") + List issues = (List) newInput; + for (ScanIssue issue : issues) { + String fileName = extractFileName(issue.getFilePath()); + fileToIssues.computeIfAbsent(fileName, k -> new ArrayList<>()).add(issue); + } + } + } + + @Override + public Object[] getElements(Object inputElement) { + return fileToIssues.keySet().toArray(); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof String) { + List issues = fileToIssues.get(parentElement); + return issues != null ? issues.toArray() : new Object[0]; + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) element; + return extractFileName(issue.getFilePath()); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof String) { + List issues = fileToIssues.get(element); + return issues != null && !issues.isEmpty(); + } + return false; + } + + private String extractFileName(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return "Unknown"; + } + int lastSeparator = Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')); + return lastSeparator >= 0 ? filePath.substring(lastSeparator + 1) : filePath; + } + + @Override + public void dispose() { + fileToIssues.clear(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java new file mode 100644 index 00000000..285de9b7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java @@ -0,0 +1,76 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignored; + +import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.StyledString.Styler; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.TextStyle; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; + +/** + * Label provider for ignored problems tree view. Renders severity icons and + * formatted text with strikethrough styling to indicate ignored status. + */ +public class IgnoredProblemsLabelProvider extends DelegatingStyledCellLabelProvider { + + public IgnoredProblemsLabelProvider() { + super(new IStyledLabelProvider() { + @Override + public StyledString getStyledText(Object element) { + if (element instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) element; + String lineNum = issue.getLocations() != null && !issue.getLocations().isEmpty() + ? String.valueOf(issue.getLocations().get(0).getLine()) : "?"; + String text = "[" + issue.getSeverity().toUpperCase() + "] " + issue.getTitle() + + " (Line " + lineNum + ")"; + StyledString styledText = new StyledString(text); + // Strikethrough style for ignored problems + Styler strikethrough = new Styler() { + @Override + public void applyStyles(TextStyle textStyle) { + textStyle.strikeout = true; + } + }; + styledText.setStyle(0, text.length(), strikethrough); + return styledText; + } else if (element instanceof String) { + return new StyledString((String) element, StyledString.QUALIFIER_STYLER); + } + return new StyledString(""); + } + + @Override + public Image getImage(Object element) { + if (element instanceof ScanIssue) { + ScanIssue issue = (ScanIssue) element; + if (issue.getSeverity() != null) { + try { + return IconRegistry.getIcon(issue.getSeverity(), IconRegistry.Size.SMALL); + } catch (Exception e) { + return null; + } + } + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) {} + + @Override + public void removeListener(ILabelProviderListener listener) {} + + @Override + public void dispose() {} + + @Override + public boolean isLabelProperty(Object element, String property) { + return false; + } + }); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java new file mode 100644 index 00000000..e4404b00 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java @@ -0,0 +1,212 @@ +package com.checkmarx.eclipse.devassist.ui.findings.ignored; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.runtime.preferences.ConfigurationScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.osgi.service.prefs.BackingStoreException; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Persistent storage for ignored problems. Uses Eclipse preferences to store + * ignored problem IDs. Provides thread-safe access to ignored problems list. + */ +public class IgnoredProblemsStore { + + private static final String PLUGIN_ID = "com.checkmarx.ast.eclipse"; + private static final String PREF_IGNORED_PROBLEMS = "ignoredProblems"; + private static final String SEPARATOR = ","; + + private static final IgnoredProblemsStore INSTANCE = new IgnoredProblemsStore(); + private final Set ignoredProblemIds = Collections.synchronizedSet(new HashSet<>()); + private final Map ignoredProblemsCache = Collections.synchronizedMap(new HashMap<>()); + private final List listeners = Collections.synchronizedList(new ArrayList<>()); + + private IgnoredProblemsStore() { + loadFromPreferences(); + } + + public static IgnoredProblemsStore getInstance() { + return INSTANCE; + } + + /** + * Add a problem to the ignored list (by ID only). + */ + public void ignoreProblem(String problemId) { + if (problemId != null && ignoredProblemIds.add(problemId)) { + System.out.println("[IGNORED-STORE] Added to ignored: " + problemId); + saveToPreferences(); + notifyListeners(); + } + } + + /** + * Add a finding to the ignored list with full finding details. + * This allows findings from the Findings View to be properly displayed in the Ignored Problems View. + */ + public void ignoreProblem(ScanIssue issue) { + if (issue != null && issue.getScanIssueId() != null) { + System.out.println("[IGNORED-STORE] ignoreProblem(ScanIssue) called with ID: " + issue.getScanIssueId()); + ignoreProblem(issue.getScanIssueId()); + // Cache the full issue details for later retrieval + ignoredProblemsCache.put(issue.getScanIssueId(), issue); + System.out.println("[IGNORED-STORE] ✓ Cached issue details. Cache size: " + ignoredProblemsCache.size()); + } else { + System.out.println("[IGNORED-STORE] ✗ ERROR: issue is null or ID is null!"); + } + } + + /** + * Remove a problem from the ignored list (restore it). + */ + public void restoreProblem(String problemId) { + if (problemId != null && ignoredProblemIds.remove(problemId)) { + System.out.println("[IGNORED-STORE] Removed from ignored: " + problemId); + ignoredProblemsCache.remove(problemId); + saveToPreferences(); + notifyListeners(); + } + } + + /** + * Check if a problem is ignored. + */ + public boolean isIgnored(String problemId) { + return problemId != null && ignoredProblemIds.contains(problemId); + } + + /** + * Get all ignored problem IDs. + */ + public Set getIgnoredProblemIds() { + return new HashSet<>(ignoredProblemIds); + } + + /** + * Filter a list of issues, returning only non-ignored ones. + */ + public List filterActiveProblems(List issues) { + List active = new ArrayList<>(); + for (ScanIssue issue : issues) { + if (!isIgnored(issue.getScanIssueId())) { + active.add(issue); + } + } + return active; + } + + /** + * Get only ignored issues from a list. + */ + public List getIgnoredProblems(List allIssues) { + List ignored = new ArrayList<>(); + for (ScanIssue issue : allIssues) { + if (isIgnored(issue.getScanIssueId())) { + ignored.add(issue); + } + } + return ignored; + } + + /** + * Get all ignored issues including cached findings from the Findings View. + * Combines issues from the provided list with cached issue details. + */ + public List getAllIgnoredProblems(List allIssues) { + List result = new ArrayList<>(); + + // First add ignored issues from the provided list + if (allIssues != null) { + for (ScanIssue issue : allIssues) { + if (isIgnored(issue.getScanIssueId())) { + result.add(issue); + } + } + } + + // Then add any cached issues not yet in the result (e.g., findings from Findings View) + for (Map.Entry entry : ignoredProblemsCache.entrySet()) { + if (isIgnored(entry.getKey()) && !result.stream().anyMatch(i -> i.getScanIssueId().equals(entry.getKey()))) { + result.add(entry.getValue()); + } + } + + return result; + } + + /** + * Clear all ignored problems. + */ + public void clearAll() { + ignoredProblemIds.clear(); + ignoredProblemsCache.clear(); + saveToPreferences(); + notifyListeners(); + System.out.println("[IGNORED-STORE] Cleared all ignored problems"); + } + + /** + * Register listener for ignore/restore events. + */ + public void addListener(IgnoredProblemsListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + /** + * Unregister listener. + */ + public void removeListener(IgnoredProblemsListener listener) { + listeners.remove(listener); + } + + private void notifyListeners() { + for (IgnoredProblemsListener listener : listeners) { + listener.onIgnoredProblemsChanged(); + } + } + + private void loadFromPreferences() { + try { + IEclipsePreferences prefs = ConfigurationScope.INSTANCE.getNode(PLUGIN_ID); + String ignored = prefs.get(PREF_IGNORED_PROBLEMS, ""); + if (!ignored.isEmpty()) { + String[] ids = ignored.split(SEPARATOR); + for (String id : ids) { + if (!id.trim().isEmpty()) { + ignoredProblemIds.add(id.trim()); + } + } + System.out.println("[IGNORED-STORE] Loaded " + ignoredProblemIds.size() + " ignored problems from preferences"); + } + } catch (Exception e) { + System.err.println("[IGNORED-STORE] Error loading preferences: " + e.getMessage()); + } + } + + private void saveToPreferences() { + try { + IEclipsePreferences prefs = ConfigurationScope.INSTANCE.getNode(PLUGIN_ID); + String ignored = String.join(SEPARATOR, ignoredProblemIds); + prefs.put(PREF_IGNORED_PROBLEMS, ignored); + prefs.flush(); + System.out.println("[IGNORED-STORE] Saved " + ignoredProblemIds.size() + " ignored problems to preferences"); + } catch (BackingStoreException e) { + System.err.println("[IGNORED-STORE] Error saving preferences: " + e.getMessage()); + } + } + + public interface IgnoredProblemsListener { + void onIgnoredProblemsChanged(); + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java new file mode 100644 index 00000000..a1b88bda --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java @@ -0,0 +1,381 @@ +package com.checkmarx.eclipse.devassist.ui.findings.integration; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.statushandlers.StatusManager; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.IStatus; +import java.util.HashMap; +import java.util.Map; + +import com.checkmarx.eclipse.Activator; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Integration with GitHub Copilot for Eclipse. + * + * Attempts to send prompts to Copilot chat via available commands. + * Falls back to clipboard if Copilot is unavailable. + * + * Implementation Strategy: + * 1. Try to find and execute Copilot commands (70% probability they exist) + * 2. If commands not available, copy prompt to clipboard + * 3. User can manually paste into Copilot Chat + * + * This ensures users always have the prompt available, either: + * - Sent automatically to Copilot (best case) + * - In clipboard for manual paste (fallback) + */ +public class CopilotIntegration { + + private static final String LOG_PREFIX = "[COPILOT-INTEGRATION]"; + + /** + * Known Copilot command IDs to try (in priority order) + * Using Microsoft Copilot for Eclipse commands + */ + private static final String COPILOT_OPEN_COMMAND = "com.microsoft.copilot.eclipse.commands.openChatView"; + private static final String COPILOT_INPUT_PARAM = "com.microsoft.copilot.eclipse.commands.openChatView.inputValue"; + private static final String COPILOT_AUTO_SEND_PARAM = "com.microsoft.copilot.eclipse.commands.openChatView.autoSend"; + + /** + * Send a prompt to Copilot Chat. + * + * Smart fallback strategy: + * 1. Try to send via Copilot command (best case) + * 2. If command fails, copy to clipboard (fallback) + * 3. Show appropriate notification based on what succeeded + * + * @param prompt The prompt to send to Copilot + * @return true if successfully sent via command or clipboard + */ + public static boolean sendPromptToCopilot(String prompt) { + if (prompt == null || prompt.isEmpty()) { + CxLogger.error("Cannot send empty prompt to Copilot", new Exception("Empty prompt")); + return false; + } + + CxLogger.info(LOG_PREFIX + " Attempting to send prompt to Copilot..."); + + // Step 1: Try to send prompt via Copilot command + boolean copilotSuccess = tryPasteAndSendToCopilot(prompt); + + if (copilotSuccess) { + CxLogger.info(LOG_PREFIX + " ✓ Successfully sent prompt via Copilot command"); + return true; + } + + // Step 2: Fallback - copy to clipboard if command failed + CxLogger.warning(LOG_PREFIX + " Copilot command failed - falling back to clipboard"); + boolean clipboardSuccess = copyToClipboard(prompt); + + if (clipboardSuccess) { + CxLogger.info(LOG_PREFIX + " ✓ Prompt copied to clipboard as fallback"); + showNotification( + "Fix Prompt Copied (Copilot Unavailable)", + "The Copilot command is not available.\n\n" + + "✓ The fix prompt has been copied to your clipboard.\n\n" + + "Open Microsoft Copilot Chat and paste (Ctrl+V) to get AI-powered fix suggestions.", + IStatus.INFO + ); + return true; // Success because clipboard worked + } + + // Both methods failed + CxLogger.error(LOG_PREFIX + " Failed to send prompt - both command and clipboard failed", + new Exception("Copilot command and clipboard fallback both failed")); + showNotification( + "Failed to Send Prompt", + "Could not send prompt to Copilot Chat or copy to clipboard.\n\n" + + "Please check that Copilot is properly installed.", + IStatus.WARNING + ); + return false; + } + + /** + * Tries to paste the prompt into Copilot Chat and trigger send + * + * @param prompt The prompt to paste + * @return true if successfully pasted, false otherwise + */ + private static boolean tryPasteAndSendToCopilot(String prompt) { + final boolean[] success = { false }; + + try { + // Step 1: Try to execute Copilot open command with prompt + CxLogger.info(LOG_PREFIX + " Attempting to execute Copilot open command with prompt..."); + boolean commandExecuted = executeOpenCopilotCommand(prompt); + if (commandExecuted) { + CxLogger.info(LOG_PREFIX + " ✓ Copilot command executed successfully - prompt sent!"); + return true; // Success - command handled it all + } else { + CxLogger.warning(LOG_PREFIX + " Copilot command execution failed - attempting manual paste fallback"); + } + + // Step 2: Try to find and paste into the view + Display.getDefault().syncExec(() -> { + try { + IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (activeWindow == null) { + CxLogger.warning(LOG_PREFIX + " No active workbench window"); + return; + } + + IWorkbenchPage activePage = activeWindow.getActivePage(); + if (activePage == null) { + CxLogger.warning(LOG_PREFIX + " No active workbench page"); + return; + } + + // Try known Copilot view IDs + String[] copilotViewIds = { + "GitHub.Copilot.Chat.View", + "com.github.copilot.chat.view", + "copilot.chatView", + "com.github.copilot.views.CopilotChatView" + }; + + IViewPart copilotView = null; + String foundViewId = null; + for (String viewId : copilotViewIds) { + try { + copilotView = activePage.findView(viewId); + if (copilotView != null) { + foundViewId = viewId; + CxLogger.info(LOG_PREFIX + " Found Copilot Chat view: " + viewId); + break; + } + } catch (Exception e) { + // Try next + } + } + + if (copilotView == null) { + CxLogger.warning(LOG_PREFIX + " Could not find Copilot Chat view after command execution"); + return; + } + + // Activate the view and bring to front + activePage.activate(copilotView); + activeWindow.getShell().forceActive(); + CxLogger.info(LOG_PREFIX + " Activated Copilot Chat view: " + foundViewId); + + // Find the text input field + Control control = copilotView.getAdapter(Control.class); + if (control == null || control.isDisposed()) { + CxLogger.warning(LOG_PREFIX + " Could not get control from Copilot view"); + return; + } + + Text inputField = findTextInputField(control); + if (inputField == null || inputField.isDisposed()) { + CxLogger.warning(LOG_PREFIX + " Could not find text input field in Copilot Chat"); + return; + } + + // Focus and set the content + inputField.setFocus(); + inputField.setText(prompt); + + CxLogger.info(LOG_PREFIX + " ✓ Successfully pasted prompt into Copilot Chat input field"); + success[0] = true; + + // Note: We don't auto-send to give user a chance to review + // User can press Enter or click Send button manually + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error pasting to Copilot Chat: " + e.getMessage()); + e.printStackTrace(); + } + }); + + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Error in tryPasteAndSendToCopilot: " + e.getMessage(), e); + } + + return success[0]; + } + + /** + * Executes Microsoft Copilot for Eclipse command to open chat with prompt + * + * @param prompt The prompt to send to Copilot + * @return true if command was executed successfully, false otherwise + */ + private static boolean executeOpenCopilotCommand(String prompt) { + final boolean[] success = { false }; + + try { + Display.getDefault().syncExec(() -> { + try { + // Get the command service + ICommandService commandService = PlatformUI.getWorkbench() + .getService(ICommandService.class); + + if (commandService == null) { + CxLogger.warning(LOG_PREFIX + " ICommandService not available"); + return; + } + + // Get the Copilot open command + Command command = commandService.getCommand(COPILOT_OPEN_COMMAND); + + if (command == null) { + CxLogger.warning(LOG_PREFIX + " Copilot command not found: " + COPILOT_OPEN_COMMAND); + return; + } + + if (!command.isEnabled()) { + CxLogger.warning(LOG_PREFIX + " Copilot command is not enabled"); + return; + } + + CxLogger.info(LOG_PREFIX + " Found Copilot command: " + COPILOT_OPEN_COMMAND); + + // Create parameters map for the command + java.util.Map parameters = new java.util.HashMap<>(); + parameters.put(COPILOT_INPUT_PARAM, prompt); + parameters.put(COPILOT_AUTO_SEND_PARAM, "true"); + + // Execute the command with parameters + try { + System.out.println("=== BEFORE EXECUTE ==="); + command.executeWithChecks(new ExecutionEvent( + command, + parameters, + null, + null + )); + + System.out.println("=== AFTER EXECUTE ==="); + + + CxLogger.info(LOG_PREFIX + " ✓ Successfully executed Copilot command with prompt"); + success[0] = true; + + System.out.println("=== SUCCESS SET TRUE ==="); + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Command execution failed: " + e.getMessage()); + e.printStackTrace(); + } + + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Error executing Copilot command: " + e.getMessage()); + } + }); + + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Error in executeOpenCopilotCommand: " + e.getMessage(), e); + } + System.out.println("executeOpenCopilotCommand returning = " + success[0]); + return success[0]; + } + + + /** + * Recursively searches for a Text widget that appears to be the chat input (fallback) + * + * @param control The control to search + * @return The text widget if found, null otherwise + */ + private static Text findTextInputField(Control control) { + if (control == null || control.isDisposed()) { + return null; + } + + // If this is a Text widget, it might be the input field + if (control instanceof Text) { + Text text = (Text) control; + // Look for text widget that's editable and multi-line + if (!text.isDisposed() && (text.getStyle() & SWT.MULTI) != 0) { + return text; + } + } + + // Recursively search children if this is a composite + if (control instanceof Composite) { + Composite composite = (Composite) control; + Control[] children = composite.getChildren(); + for (Control child : children) { + Text found = findTextInputField(child); + if (found != null) { + return found; + } + } + } + + return null; + } + + /** + * Copies text to system clipboard + * + * @param text The text to copy + * @return true if successful, false otherwise + */ + private static boolean copyToClipboard(String text) { + try { + Display display = Display.getDefault(); + + display.syncExec(() -> { + Clipboard clipboard = new Clipboard(display); + TextTransfer transfer = TextTransfer.getInstance(); + clipboard.setContents(new Object[] { text }, new org.eclipse.swt.dnd.Transfer[] { transfer }); + clipboard.dispose(); + }); + + CxLogger.info(LOG_PREFIX + " Text copied to clipboard"); + return true; + } catch (Exception e) { + CxLogger.error(LOG_PREFIX + " Failed to copy to clipboard: " + e.getMessage(), e); + return false; + } + } + + /** + * Shows a notification to the user + * + * @param title The notification title + * @param message The notification message + * @param severity The severity (IStatus.INFO, IStatus.WARNING, etc) + */ + private static void showNotification(String title, String message, int severity) { + try { + Display.getDefault().asyncExec(() -> { + IStatus status = new Status( + severity, + Activator.PLUGIN_ID, + title + "\n" + message + ); + StatusManager.getManager().handle(status, StatusManager.SHOW | StatusManager.LOG); + }); + } catch (Exception e) { + CxLogger.warning(LOG_PREFIX + " Failed to show notification: " + e.getMessage()); + } + } + + /** + * Checks if Copilot is available in this Eclipse instance + * Currently returns false as we use clipboard method as primary approach + * + * @return true if Copilot is available + */ + public static boolean isCopilotAvailable() { + // Clipboard method is always available as fallback + return false; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java new file mode 100644 index 00000000..b5dee657 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/RemediationPromptBuilder.java @@ -0,0 +1,274 @@ +package com.checkmarx.eclipse.devassist.ui.findings.integration; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +/** + * Builds engine-specific remediation prompts for Copilot. + * + * Generates context-aware prompts based on vulnerability type: + * - ASCA (SAST): Code analysis with line numbers and remediation advice + * - OSS (Dependencies): Package information and upgrade paths + * - SECRETS: Credential type and revocation steps + * - CONTAINERS: Image information and vulnerability sources + * - IAC: Configuration details and expected vs actual values + * + * Each prompt type is optimized for the scan engine context. + */ +public class RemediationPromptBuilder { + + /** + * Build a remediation prompt for the given scan issue + * + * @param issue The scan issue to build prompt for + * @return The remediation prompt, or empty string if unable to build + */ + public static String buildRemediationPrompt(ScanIssue issue) { + if (issue == null) { + return ""; + } + + ScanEngine engine = issue.getScanEngine(); + + switch (engine) { + case ASCA: + return buildASCAPrompt(issue); + case OSS: + return buildOSSPrompt(issue); + case SECRETS: + return buildSecretPrompt(issue); + case CONTAINERS: + return buildContainerPrompt(issue); + case IAC: + return buildIACPrompt(issue); + default: + return buildGenericPrompt(issue); + } + } + + /** + * Build prompt for ASCA (SAST) vulnerabilities + * Context: Code analysis with line numbers and severity + */ + private static String buildASCAPrompt(ScanIssue issue) { + return String.format( + "You are a code security expert. Please fix the following code vulnerability:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**Line Number:** %d\n" + + "**File:** %s\n" + + "%s" + // Remediation advice if available + "\n**Requirements for the fix:**\n" + + "1. Must address the security vulnerability\n" + + "2. Should maintain code readability\n" + + "3. Must preserve existing functionality\n" + + "4. Include necessary imports/dependencies\n" + + "5. Follow Java best practices\n\n" + + "Provide the corrected code snippet.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getProblematicLineNumber(), + issue.getFilePath(), + buildRemediationAdviceSection(issue) + ); + } + + /** + * Build prompt for OSS (Open Source Software) vulnerabilities + * Context: Dependency vulnerabilities with version information + */ + private static String buildOSSPrompt(ScanIssue issue) { + return String.format( + "A vulnerability was detected in an open source dependency:\n\n" + + "**Package:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "%s" + // Version info if available + "\n**What needs to be done:**\n" + + "1. Identify recommended safe version to upgrade to\n" + + "2. Provide upgrade command (Maven/Gradle/npm/pip as applicable)\n" + + "3. List any configuration changes needed\n" + + "4. Note any compatibility concerns\n" + + "5. Suggest testing approach\n\n" + + "Please provide step-by-step remediation instructions.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + buildVersionInfo(issue) + ); + } + + /** + * Build prompt for SECRETS (credential leaks) + * Context: Exposed credentials that need immediate action + */ + private static String buildSecretPrompt(ScanIssue issue) { + return String.format( + "**SECURITY ALERT:** A secret/credential has been detected in the code:\n\n" + + "**Secret Type:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n\n" + + "**Immediate Actions Required:**\n" + + "1. If this is a real credential, immediately revoke/rotate it in the management console\n" + + "2. Generate new credentials if necessary\n" + + "3. Update application configuration to use new credentials\n" + + "4. Remove the hardcoded credential from source code\n\n" + + "**Best Practices to Prevent:**\n" + + "- Use environment variables or secrets manager\n" + + "- Never commit secrets to version control\n" + + "- Use tools like GitGuardian or TruffleHog to scan commits\n" + + "- Implement pre-commit hooks to prevent secret commits\n\n" + + "Provide detailed steps for proper secret management.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath() + ); + } + + /** + * Build prompt for CONTAINERS (container image vulnerabilities) + * Context: Docker/container image vulnerabilities + */ + private static String buildContainerPrompt(ScanIssue issue) { + return String.format( + "A vulnerability was detected in a container image:\n\n" + + "**Image/Library:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n" + + "%s" + // Version info if available + "\n**Remediation Steps:**\n" + + "1. Use specific version tags instead of 'latest'\n" + + "2. Update base image to patched version\n" + + "3. Use minimal base images (alpine, distroless)\n" + + "4. Implement scanning in CI/CD pipeline\n" + + "5. Regular image updates and patching\n\n" + + "Provide updated Dockerfile snippet and best practices.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath(), + buildVersionInfo(issue) + ); + } + + /** + * Build prompt for IAC (Infrastructure as Code) issues + * Context: Configuration and infrastructure vulnerabilities + */ + private static String buildIACPrompt(ScanIssue issue) { + return String.format( + "An infrastructure configuration vulnerability was detected:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n" + + "**Type:** Infrastructure as Code (Terraform/CloudFormation/YAML)\n" + + "%s" + // Configuration details if available + "\n**Security Requirements:**\n" + + "1. Enable encryption where applicable\n" + + "2. Enforce authentication and authorization\n" + + "3. Use least privilege principles\n" + + "4. Enable logging and monitoring\n" + + "5. Follow cloud provider security best practices\n\n" + + "Provide corrected configuration and explanation of security improvements.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath(), + buildConfigInfo(issue) + ); + } + + /** + * Generic prompt for unknown engine types + */ + private static String buildGenericPrompt(ScanIssue issue) { + return String.format( + "A security issue was detected in the code:\n\n" + + "**Issue:** %s\n" + + "**Severity:** %s\n" + + "**Description:** %s\n" + + "**File:** %s\n\n" + + "Please provide:\n" + + "1. Root cause analysis\n" + + "2. Security implications\n" + + "3. Step-by-step fix\n" + + "4. Prevention strategies\n\n" + + "Ensure the fix follows security best practices.", + + issue.getTitle(), + formatSeverity(issue.getSeverity()), + issue.getDescription(), + issue.getFilePath() + ); + } + + /** + * Helper: Format severity level + */ + private static String formatSeverity(String severity) { + if (severity == null) return "UNKNOWN"; + switch (severity.toUpperCase()) { + case "CRITICAL": + return "🔴 CRITICAL - Requires immediate attention"; + case "HIGH": + return "🟠 HIGH - Should be fixed soon"; + case "MEDIUM": + return "🟡 MEDIUM - Should be addressed"; + case "LOW": + return "🟢 LOW - May be fixed in next iteration"; + default: + return severity; + } + } + + /** + * Helper: Build remediation advice section if available + */ + private static String buildRemediationAdviceSection(ScanIssue issue) { + String advice = issue.getRemediationAdvise(); + if (advice != null && !advice.isEmpty()) { + return "\n**Remediation Advice:** " + advice; + } + return ""; + } + + /** + * Helper: Build version information section + */ + private static String buildVersionInfo(ScanIssue issue) { + StringBuilder sb = new StringBuilder(); + + String currentVersion = issue.getPackageVersion(); + if (currentVersion != null && !currentVersion.isEmpty()) { + sb.append("\n**Current Version:** ").append(currentVersion); + } + + // Note: Recommended version may be available from API in future versions + // For now, we rely on Copilot to suggest the safe version + + return sb.toString(); + } + + /** + * Helper: Build configuration details section + */ + private static String buildConfigInfo(ScanIssue issue) { + String description = issue.getDescription(); + if (description != null && description.length() > 100) { + return "\n**Details:** " + description; + } + return ""; + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java new file mode 100644 index 00000000..1956b594 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java @@ -0,0 +1,196 @@ +package com.checkmarx.eclipse.devassist.ui.findings.marker; + +import org.eclipse.core.resources.IMarker; + +import com.checkmarx.eclipse.enums.Severity; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Maps between ScanIssue objects and IMarker attributes. + * This is the single source of truth for marker attribute serialization. + * Allows marker resolution to reconstruct finding details without searching. + */ +public class MarkerIssueMapper { + + // Marker attribute names (prefixed with cx. to avoid collision) + private static final String ATTR_ISSUE_ID = "cx.issueId"; + private static final String ATTR_SEVERITY = "cx.severity"; + private static final String ATTR_TITLE = "cx.title"; + private static final String ATTR_DESCRIPTION = "cx.description"; + private static final String ATTR_REMEDIATION = "cx.remediation"; + private static final String ATTR_RULE_ID = "cx.ruleId"; + private static final String ATTR_FILE_PATH = "cx.filePath"; + private static final String ATTR_SCAN_ENGINE = "cx.scanEngine"; + + /** + * Reconstruct a ScanIssue from marker attributes. + * Called by marker resolution to populate the details dialog. + * + * @param marker the IMarker containing serialized issue data + * @return reconstructed ScanIssue, or null if reconstruction fails + */ + public static ScanIssue fromMarker(IMarker marker) { + try { + String issueId = marker.getAttribute(ATTR_ISSUE_ID, ""); + String severity = marker.getAttribute(ATTR_SEVERITY, "MEDIUM"); + String title = marker.getAttribute(ATTR_TITLE, marker.getAttribute(IMarker.MESSAGE, "")); + String description = marker.getAttribute(ATTR_DESCRIPTION, ""); + String remediation = marker.getAttribute(ATTR_REMEDIATION, null); + Integer ruleId = null; + try { + Object ruleIdObj = marker.getAttribute(ATTR_RULE_ID); + if (ruleIdObj instanceof Integer) { + ruleId = (Integer) ruleIdObj; + } else if (ruleIdObj instanceof String && !ruleIdObj.toString().isEmpty()) { + ruleId = Integer.parseInt(ruleIdObj.toString()); + } + } catch (Exception e) { + // Keep ruleId as null + } + String filePath = marker.getAttribute(ATTR_FILE_PATH, ""); + String scanEngineStr = marker.getAttribute(ATTR_SCAN_ENGINE, "ASCA"); + int lineNumber = marker.getAttribute(IMarker.LINE_NUMBER, 1); + int charStart = marker.getAttribute(IMarker.CHAR_START, 0); + int charEnd = marker.getAttribute(IMarker.CHAR_END, 0); + + // Reconstruct ScanIssue + ScanIssue issue = new ScanIssue(); + issue.setScanIssueId(issueId); + issue.setSeverity(severity); + issue.setTitle(title); + issue.setDescription(description); + issue.setRemediationAdvise(remediation); + issue.setRuleId(ruleId); + issue.setFilePath(filePath); + + // Parse scan engine + try { + issue.setScanEngine(ScanEngine.valueOf(scanEngineStr)); + } catch (IllegalArgumentException e) { + issue.setScanEngine(ScanEngine.ASCA); + } + + // Reconstruct location + Location location = new Location(); + location.setLine(lineNumber); + location.setStartIndex(charStart); + location.setEndIndex(charEnd); + issue.setLocations(java.util.Collections.singletonList(location)); + + return issue; + } catch (Exception e) { + System.out.println("[MARKER-MAPPER] Error reconstructing ScanIssue from marker: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * Populate marker attributes from a ScanIssue. + * Called when creating markers from findings. + * + * @param marker the IMarker to populate + * @param issue the ScanIssue containing data to serialize + */ + public static void populateMarker(IMarker marker, ScanIssue issue) { + try { + if (issue.getScanIssueId() != null && !issue.getScanIssueId().isEmpty()) { + marker.setAttribute(ATTR_ISSUE_ID, issue.getScanIssueId()); + } + + if (issue.getSeverity() != null && !issue.getSeverity().isEmpty()) { + marker.setAttribute(ATTR_SEVERITY, issue.getSeverity()); + } + + if (issue.getTitle() != null && !issue.getTitle().isEmpty()) { + marker.setAttribute(ATTR_TITLE, issue.getTitle()); + // Also set MESSAGE for default marker hover display + marker.setAttribute(IMarker.MESSAGE, issue.getTitle()); + } + + if (issue.getDescription() != null && !issue.getDescription().isEmpty()) { + marker.setAttribute(ATTR_DESCRIPTION, issue.getDescription()); + } + + if (issue.getRemediationAdvise() != null && !issue.getRemediationAdvise().isEmpty()) { + marker.setAttribute(ATTR_REMEDIATION, issue.getRemediationAdvise()); + } + + if (issue.getRuleId() != null) { + marker.setAttribute(ATTR_RULE_ID, issue.getRuleId()); + } + + if (issue.getFilePath() != null && !issue.getFilePath().isEmpty()) { + marker.setAttribute(ATTR_FILE_PATH, issue.getFilePath()); + } + + if (issue.getScanEngine() != null) { + marker.setAttribute(ATTR_SCAN_ENGINE, issue.getScanEngine().toString()); + } + + // Set standard marker attributes from location + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location location = issue.getLocations().get(0); + marker.setAttribute(IMarker.LINE_NUMBER, location.getLine()); + marker.setAttribute(IMarker.CHAR_START, location.getStartIndex()); + marker.setAttribute(IMarker.CHAR_END, location.getEndIndex()); + + // Calculate severity for Eclipse marker system (0=info, 1=warning, 2=error) + int severity = calculateMarkerSeverity(issue.getSeverity()); + marker.setAttribute(IMarker.SEVERITY, severity); + } + + System.out.println("[MARKER-MAPPER] Populated marker for issue: " + issue.getTitle()); + + } catch (Exception e) { + System.out.println("[MARKER-MAPPER] Error populating marker: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Convert Checkmarx severity to Eclipse marker severity level. + */ + private static int calculateMarkerSeverity(String severity) { + if (severity == null) { + return IMarker.SEVERITY_WARNING; + } + + switch (severity.toLowerCase()) { + case "critical": + case "high": + return IMarker.SEVERITY_ERROR; + case "medium": + return IMarker.SEVERITY_WARNING; + case "low": + case "info": + return IMarker.SEVERITY_INFO; + default: + return IMarker.SEVERITY_WARNING; + } + } + + /** + * Convert Checkmarx Severity enum to Eclipse marker severity level. + */ + private static int toEclipseSeverity(Severity severity) { + if (severity == null) { + return IMarker.SEVERITY_WARNING; + } + + switch (severity) { + case CRITICAL: + case HIGH: + return IMarker.SEVERITY_ERROR; + case MEDIUM: + return IMarker.SEVERITY_WARNING; + case LOW: + case INFO: + default: + return IMarker.SEVERITY_INFO; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java new file mode 100644 index 00000000..2f9da0d2 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java @@ -0,0 +1,82 @@ +package com.checkmarx.eclipse.devassist.ui.findings.model; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import org.eclipse.swt.graphics.Image; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * Represents a file node in the findings tree. + * Contains file metadata, issue counts grouped by severity, and file type icon. + * Icon is resolved at node creation time, following the JetBrains plugin pattern. + */ +public class FileNodeLabel { + + private final String fileName; + private final String filePath; + private final List issues; + private final Map problemCount; + private final Image icon; + + public FileNodeLabel(String fileName, String filePath, List issues) { + this(fileName, filePath, issues, null, null); + } + + public FileNodeLabel(String fileName, String filePath, List issues, Image icon) { + this(fileName, filePath, issues, calculateProblemCount(issues), icon); + } + + public FileNodeLabel(String fileName, String filePath, List issues, Map problemCount, Image icon) { + this.fileName = fileName; + this.filePath = filePath; + this.issues = issues; + this.problemCount = problemCount != null ? problemCount : calculateProblemCount(issues); + this.icon = icon; + } + + /** + * Calculate problem counts grouped by severity. + */ + private static Map calculateProblemCount(List issues) { + Map counts = new HashMap<>(); + + if (issues == null || issues.isEmpty()) { + return counts; + } + + for (ScanIssue issue : issues) { + String severity = issue.getSeverity(); + if (severity != null) { + counts.put(severity, counts.getOrDefault(severity, 0L) + 1); + } + } + + return counts; + } + + public String getFileName() { + return fileName; + } + + public String getFilePath() { + return filePath; + } + + public List getIssues() { + return issues; + } + + public Map getProblemCount() { + return problemCount; + } + + public Image getIcon() { + return icon; + } + + @Override + public String toString() { + return fileName; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java new file mode 100644 index 00000000..b0c4be77 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/ScanDetailWithPath.java @@ -0,0 +1,31 @@ +package com.checkmarx.eclipse.devassist.ui.findings.model; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Represents a scan issue with its associated file path. + * Used as a leaf node in the findings tree. + */ +public class ScanDetailWithPath { + + private final ScanIssue detail; + private final String filePath; + + public ScanDetailWithPath(ScanIssue detail, String filePath) { + this.detail = detail; + this.filePath = filePath; + } + + public ScanIssue getDetail() { + return detail; + } + + public String getFilePath() { + return filePath; + } + + @Override + public String toString() { + return detail != null ? detail.getTitle() : "Unknown"; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java new file mode 100644 index 00000000..9d81893a --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java @@ -0,0 +1,142 @@ +package com.checkmarx.eclipse.devassist.ui.findings.provider; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.IEditorRegistry; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; + +import java.util.List; +import java.util.Map; + +/** + * Content provider for the Findings tree viewer. + * Implements {@link ITreeContentProvider} to provide hierarchical content structure. + * Organizes scan issues by file path as parent nodes with individual issues as children. + */ +public class FindingsContentProvider implements ITreeContentProvider { + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Map) { + @SuppressWarnings("unchecked") + Map> map = (Map>) inputElement; + + System.out.println("[FINDINGS-CONTENT] ========================================"); + System.out.println("[FINDINGS-CONTENT] Creating FileNodeLabel elements..."); + System.out.println("[FINDINGS-CONTENT] Input files: " + map.size()); + + Object[] elements = map.entrySet().stream() + .map(entry -> { + String fileName = getFileName(entry.getKey()); + Image fileIcon = getFileIcon(fileName); + List issues = entry.getValue(); + + System.out.println("[FINDINGS-CONTENT] File: " + fileName); + System.out.println("[FINDINGS-CONTENT] Path: " + entry.getKey()); + System.out.println("[FINDINGS-CONTENT] Issues: " + issues.size()); + + // Calculate and log severity counts + java.util.Map counts = new java.util.HashMap<>(); + for (ScanIssue issue : issues) { + String severity = issue.getSeverity(); + counts.put(severity, counts.getOrDefault(severity, 0L) + 1); + } + counts.forEach((sev, cnt) -> + System.out.println("[FINDINGS-CONTENT] " + sev + ": " + cnt) + ); + + return new FileNodeLabel( + fileName, + entry.getKey(), + issues, + fileIcon); + }) + .toArray(); + + System.out.println("[FINDINGS-CONTENT] ✓ Created " + elements.length + " FileNodeLabel elements"); + System.out.println("[FINDINGS-CONTENT] ========================================"); + return elements; + } + + System.out.println("[FINDINGS-CONTENT] ✗ Input is not a Map, type: " + + (inputElement != null ? inputElement.getClass().getSimpleName() : "null")); + return new Object[0]; + } + + private Image getFileIcon(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return null; + } + + try { + IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry(); + ImageDescriptor imageDescriptor = registry.getImageDescriptor(fileName); + + if (imageDescriptor != null) { + Image image = imageDescriptor.createImage(); + if (image != null) { + return image; + } + } + } catch (Exception e) { + System.out.println("[FINDINGS-CONTENT] Error getting file icon for: " + fileName + " - " + e.getMessage()); + } + + return null; + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) parentElement; + return fileNode.getIssues().stream() + .map(issue -> new ScanDetailWithPath(issue, fileNode.getFilePath())) + .toArray(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof ScanDetailWithPath) { + // Parent is the file node - would need to track in the model + return null; + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof FileNodeLabel) { + return !((FileNodeLabel) element).getIssues().isEmpty(); + } + return false; + } + + private String getFileName(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return "Unknown"; + } + int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + if (lastSeparator >= 0) { + return filePath.substring(lastSeparator + 1); + } + return filePath; + } + + @Override + public void dispose() { + // Cleanup if needed + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java new file mode 100644 index 00000000..bc003450 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java @@ -0,0 +1,161 @@ +package com.checkmarx.eclipse.devassist.ui.findings.provider; + +import java.util.Map; +import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Event; + +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; +import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; + +/** + * Label provider tailored exactly to render severity shield badges + * sequentially to the right of file labels. + */ +public class FindingsLabelProvider extends DelegatingStyledCellLabelProvider { + + private static final String[] SEVERITIES = { "critical", "high", "medium", "low" }; + private static final int BETWEEN_BADGE_SPACING = 4; // Space between different shield groups + private static final int TEXT_TO_BADGE_PADDING = 28; // Space after filename before first badge + + public FindingsLabelProvider() { + super(new IStyledLabelProvider() { + @Override + public StyledString getStyledText(Object element) { + if (element instanceof FileNodeLabel) { + return new StyledString(((FileNodeLabel) element).getFileName()); + } else if (element instanceof ScanDetailWithPath) { + return new StyledString(formatIssueText(((ScanDetailWithPath) element).getDetail())); + } + return new StyledString(element.toString()); + } + + @Override + public Image getImage(Object element) { + if (element instanceof FileNodeLabel) { + return ((FileNodeLabel) element).getIcon(); + } else if (element instanceof ScanDetailWithPath) { + String severity = ((ScanDetailWithPath) element).getDetail().getSeverity(); + return IconRegistry.getIcon(severity, IconRegistry.Size.SMALL); + } + return null; + } + + @Override public void dispose() {} + @Override public void addListener(ILabelProviderListener l) {} + @Override public void removeListener(ILabelProviderListener l) {} + @Override public boolean isLabelProperty(Object el, String prop) { return false; } + + private String formatIssueText(ScanIssue detail) { + switch (detail.getScanEngine()) { + case OSS: return detail.getSeverity() + "-risk package: " + detail.getTitle() + "@" + detail.getPackageVersion() + getLineNumberText(detail); + case SECRETS: return detail.getSeverity() + "-risk secret: " + detail.getTitle() + getLineNumberText(detail); + case CONTAINERS: return detail.getSeverity() + "-risk container image: " + detail.getTitle() + ":" + detail.getImageTag() + getLineNumberText(detail); + case ASCA: + case IAC: return detail.getTitle() + getLineNumberText(detail); + default: return detail.getDescription() + getLineNumberText(detail); + } + } + + private String getLineNumberText(ScanIssue detail) { + if (detail.getLocations() != null && !detail.getLocations().isEmpty()) { + return " [Ln " + detail.getLocations().get(0).getLine() + ", Col " + detail.getLocations().get(0).getStartIndex() + "]"; + } + return ""; + } + }); + } + + @Override + protected void measure(Event event, Object element) { + super.measure(event, element); + + if (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) element; + Map counts = fileNode.getProblemCount(); + + if (counts != null && !counts.isEmpty()) { + int extraWidth = TEXT_TO_BADGE_PADDING; + for (String severity : SEVERITIES) { + if (counts.containsKey(severity) && counts.get(severity) > 0) { + String countStr = String.valueOf(counts.get(severity)); + int textWidth = event.gc.textExtent(countStr).x; + // 16px (Icon) + 4px (Gap between icon & number) + number length + gap to next badge + extraWidth += 16 + 0 + textWidth + BETWEEN_BADGE_SPACING; + } + } + event.width += extraWidth; + } + } + } + + @Override + protected void paint(Event event, Object element) { + // 1. Draw standard tree node elements (Expand/collapse arrows, file icons, text strings) + super.paint(event, element); + + // 2. Lay down the right-aligned badges + if (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) element; + Map counts = fileNode.getProblemCount(); + + if (counts != null && !counts.isEmpty()) { + // CRITICAL FIX: Reset clipping so SWT allows drawing outside the text area + org.eclipse.swt.graphics.Rectangle oldClipping = event.gc.getClipping(); + event.gc.setClipping((org.eclipse.swt.graphics.Rectangle) null); + try { + // Determine exactly where the file label ends horizontally + Point textSize = event.gc.textExtent(fileNode.getFileName()); + + // Base offset: layout context starting position + text length + margin padding + int currentX = event.x + textSize.x + TEXT_TO_BADGE_PADDING; + + int rowHeight = event.height; + int iconY = event.y + (rowHeight - 16) / 2; + int textY = event.y + (rowHeight - event.gc.getFontMetrics().getHeight()) / 2; + + for (String severity : SEVERITIES) { + Long count = counts.get(severity); + if (count != null && count > 0) { + // Grab actual shield PNG asset + Image badgePng = IconRegistry.getIcon(severity, IconRegistry.Size.SMALL); + + if (badgePng != null) { + // Draw Shield Badge + event.gc.drawImage(badgePng, currentX, iconY); + currentX += 16 + 4; // Shift right right past shield + a tiny gap + + // Draw Count Number tightly next to the shield + String countStr = String.valueOf(count); + + // Match text color dynamically (Use foreground selection color if item is highlighted) + // Match text color dynamically (Use foreground selection color if item is highlighted) + if ((event.detail & SWT.SELECTED) != 0) { + event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); + } else { + // Falls back to standard list item text color cleanly across dark/light themes + event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_FOREGROUND)); + } + + event.gc.drawString(countStr, currentX, textY, true); + + // Advance cursor layout pointer to the next shield group block + currentX += event.gc.textExtent(countStr).x + BETWEEN_BADGE_SPACING; + } + } + } + } finally { + // Restore original clipping area + event.gc.setClipping(oldClipping); + } + } + } +} +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java new file mode 100644 index 00000000..9d395d30 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java @@ -0,0 +1,89 @@ +package com.checkmarx.eclipse.devassist.ui.findings.realtime; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; + +import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler; + +/** + * Real-time document listener for Checkmarx scanning. + * + * Equivalent to JetBrains' LocalInspectionTool.buildVisitor() — detects when + * the user edits the currently opened file and triggers a real-time scan with + * debounce (1 second of inactivity). + * + * This listener observes every keystroke and delegates to DevAssistScanScheduler + * for debounced scanning coordination. + */ +public class CheckmarxDocumentListener implements IDocumentListener { + + private final RealTimeScanJob scanJob; + private final IFile file; + private final String fileName; + private final DevAssistScanScheduler scheduler; + + /** + * 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 { + // 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(); + } + } + + /** + * Dispose this listener and clean up associated resources. + * Call this when the editor is closed. + */ + public void dispose() { + if (scanJob != null) { + scanJob.cancel(); + } + } + + public String getFileName() { + return fileName; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java new file mode 100644 index 00000000..f3d31a70 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java @@ -0,0 +1,408 @@ +package com.checkmarx.eclipse.devassist.ui.findings.realtime; + +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; + +import java.util.HashMap; +import java.util.Map; + +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.problems.ProblemDecorator; + +/** + * Real-time editor listener for Checkmarx scanning. + * + * Equivalent to JetBrains' LocalInspectionTool integration — listens for editor + * open/close events and registers document listeners for real-time scanning. + * + * When a text editor opens: + * 1. Create a RealTimeScanJob for that file + * 2. Register a CheckmarxDocumentListener on the document + * 3. Every keystroke triggers the document listener + * 4. Document listener reschedules the job (1-second debounce) + * 5. When debounce expires, RealTimeScanJob.run() executes the scan + * + * When the editor closes: + * - Dispose of the document listener and cancel the job + */ +public class CheckmarxEditorListener implements IPartListener2 { + + /** + * Map of documents to their associated listeners. + * Key: IDocument hash code (unique identifier for the document) + * Value: CheckmarxDocumentListener (for cleanup on editor close) + */ + private final Map activeListeners = new HashMap<>(); + + /** + * Map of documents to their associated scan jobs. + * Key: IDocument hash code + * Value: RealTimeScanJob (for cleanup and tracking) + */ + private final Map activeScanJobs = new HashMap<>(); + + public CheckmarxEditorListener() { + System.out.println("[REALTIME] ✓ CheckmarxEditorListener created"); + } + + /** + * 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) { + System.out.println("[REALTIME] User switched to tab - triggering rescan for: " + extractFileNameFromEditor(editor)); + 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)) { + System.out.println("[REALTIME] Document listener already registered"); + return; + } + + // Get file name for logging + String fileName = extractFileNameFromEditor(editor); + System.out.println("[REALTIME] Setting up real-time scanning for: " + fileName); + + // 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) { + System.out.println("[REALTIME] Warning: Could not get scheduler from session: " + e.getMessage()); + } + } + + // 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); + + System.out.println("[REALTIME] ✓ Document listener registered for: " + fileName); + + // **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 + System.out.println("[REALTIME] Triggering initial scan for: " + fileName); + 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(); + System.out.println("[REALTIME] ✓ Document listener removed for: " + listener.getFileName()); + } 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(); + System.out.println("[REALTIME] ✓ Scan job cancelled for: " + scanJob.getFileName()); + } + } + + /** + * 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()) { + System.out.println("[REALTIME] No cached findings for: " + file.getName()); + return; + } + + // Apply decorations for cached findings + System.out.println("[REALTIME] ✓ Applying " + cachedIssues.size() + " cached decorations for: " + file.getName()); + ProblemDecorator.decorateEditor(file, cachedIssues); + + } catch (Exception e) { + System.err.println("[REALTIME] Error applying cached decorations: " + e.getMessage()); + e.printStackTrace(); + } + } + + // Implement other IPartListener2 methods (not used for real-time scanning) + + @Override + public void partBroughtToTop(IWorkbenchPartReference partRef) {} + + @Override + public void partDeactivated(IWorkbenchPartReference partRef) {} + + @Override + public void partHidden(IWorkbenchPartReference partRef) {} + + @Override + public void partVisible(IWorkbenchPartReference partRef) {} + + @Override + public void partInputChanged(IWorkbenchPartReference partRef) {} + + /** + * Get the number of active listeners (for testing/debugging). + */ + public int getActiveListenerCount() { + return activeListeners.size(); + } + + /** + * Get the number of active scan jobs (for testing/debugging). + */ + public int getActiveScanJobCount() { + return activeScanJobs.size(); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java new file mode 100644 index 00000000..2d6cedf7 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java @@ -0,0 +1,92 @@ +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.source.ISourceViewer; + +import com.checkmarx.eclipse.devassist.ui.findings.editor.CxFindingsHover; + +/** + * Listens for editor open/close events and installs custom hover handlers for Findings. + * + * This listener handles dynamic installation of hovers on editors that open during + * the session. It installs CxFindingsHover which provides rich vulnerability details + * when hovering over underlined code with FindingsAnnotation. + * + * Works independently from Eclipse's native Problems View. + */ +public class FindingsEditorHoverListener implements IPartListener2 { + + private static final CxFindingsHover findingsHover = new CxFindingsHover(); + + @Override + public void partOpened(IWorkbenchPartReference partRef) { + Object part = partRef.getPart(false); + if (part instanceof IEditorPart) { + installHoverOnEditor((IEditorPart) part); + } + } + + @Override + public void partActivated(IWorkbenchPartReference partRef) { + // Install on activation too, in case it wasn't installed earlier + Object part = partRef.getPart(false); + if (part instanceof IEditorPart) { + installHoverOnEditor((IEditorPart) part); + } + } + + @Override + public void partBroughtToTop(IWorkbenchPartReference partRef) {} + + @Override + public void partClosed(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) {} + + /** + * Install Checkmarx Findings hover handler on the given editor if it has a text viewer. + * + * Installs CxFindingsHover which finds FindingsAnnotations at the hover offset + * and displays detailed vulnerability information. + */ + public void installHoverOnEditor(IEditorPart editor) { + try { + if (editor == null) { + return; + } + + // Adapt editor to ISourceViewer to check if it's a text editor + Object viewer = null; + try { + viewer = editor.getAdapter(ISourceViewer.class); + } catch (Exception e) { + // Not a text editor; skip + return; + } + + if (viewer instanceof ISourceViewer) { + ISourceViewer sourceViewer = (ISourceViewer) viewer; + + // Register findings hover for Java code and JavaDoc content types + sourceViewer.setTextHover(findingsHover, "org.eclipse.jdt.ui.javaCode"); + sourceViewer.setTextHover(findingsHover, "org.eclipse.jdt.ui.javaDocCode"); + + System.out.println("[FINDINGS-HOVER] ✓ Installed hover on editor"); + } + } catch (Exception e) { + System.err.println("[FINDINGS-HOVER] Error installing hover: " + e.getMessage()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java new file mode 100644 index 00000000..99236a34 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java @@ -0,0 +1,244 @@ +package com.checkmarx.eclipse.devassist.ui.findings.realtime; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.Platform; + +/** + * Real-time scan job with debounce support. + * + * When the user edits a file, CheckmarxDocumentListener calls reschedule() repeatedly + * as the user types. This job cancels the previous scheduled execution and starts a + * new 1-second timer, so the scan only runs after the user pauses typing. + * + * Equivalent to: + * - JetBrains' real-time inspection pipeline (with debounce built-in) + * - Eclipse's incremental builder, but for on-demand scanning + * + * This is a background Job, so it runs off the UI thread and won't freeze the editor. + */ +public class RealTimeScanJob extends Job { + + private final IFile file; + private final String fileName; + + // Store the timestamp when the user last made changes + private long lastChangeTime = System.currentTimeMillis(); + + /** + * Create a real-time scan job for a specific file. + * + * @param file the IFile resource to scan + * @param fileName the file name (for logging) + */ + public RealTimeScanJob(IFile file, String fileName) { + super("Checkmarx Real-Time Scan: " + fileName); + this.file = file; + this.fileName = fileName; + + // Configure job properties for background execution + setSystem(false); // Show in progress view + setPriority(Job.DECORATE); // Lower priority than user interactions + setUser(false); // Not a user-initiated job + + System.out.println("[REALTIME] ✓ RealTimeScanJob created for: " + fileName); + } + + /** + * 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); + + System.out.println("[REALTIME] Job rescheduled for: " + fileName + " (delay=" + delayMs + "ms)"); + } + + /** + * 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()) { + System.out.println("[REALTIME] ✗ File no longer exists: " + fileName); + return Status.CANCEL_STATUS; + } + + // Check if the job was cancelled while waiting + if (monitor.isCanceled()) { + System.out.println("[REALTIME] ✗ Scan cancelled for: " + fileName); + return Status.CANCEL_STATUS; + } + + // **STEP 1: Check authentication status** + if (!isUserAuthenticated()) { + System.out.println("[REALTIME] ✗ BLOCKED: User not authenticated - scan cannot proceed"); + System.out.println("[REALTIME] ℹ️ User must configure API key in preferences first"); + return Status.OK_STATUS; // Return OK but don't scan + } + + System.out.println("[REALTIME] ════════════════════════════════════════"); + System.out.println("[REALTIME] ✓ Authentication verified - starting backend security scan..."); + System.out.println("[REALTIME] File: " + fileName); + System.out.println("[REALTIME] Last change: " + (System.currentTimeMillis() - lastChangeTime) + "ms ago"); + System.out.println("[REALTIME] ════════════════════════════════════════"); + + // Call our backend scanners via ScanManager + try { + org.eclipse.core.resources.IProject project = file.getProject(); + if (project == null || !project.isOpen()) { + System.out.println("[REALTIME] ✗ Project not accessible"); + return Status.OK_STATUS; + } + + String projectName = project.getName(); + org.eclipse.core.runtime.QualifiedName registryKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "scanner-registry"); + org.eclipse.core.runtime.QualifiedName stateHolderKey = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "state-holder"); + + // Get or lazily initialize backend services + com.checkmarx.eclipse.devassist.backend.ScannerRegistry registry = + (com.checkmarx.eclipse.devassist.backend.ScannerRegistry) + project.getSessionProperty(registryKey); + + com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder = + (com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder) + project.getSessionProperty(stateHolderKey); + + // Lazy initialization if not found + if (registry == null) { + System.out.println("[REALTIME] [STEP 1/5] Lazily initializing ScannerRegistry for: " + projectName); + registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project); + registry.registerAllScanners(); + project.setSessionProperty(registryKey, registry); + System.out.println("[REALTIME] ✓ ScannerRegistry initialized"); + } + + if (stateHolder == null) { + System.out.println("[REALTIME] [STEP 2/5] Lazily initializing DevAssistScanStateHolder for: " + projectName); + stateHolder = new com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder(); + project.setSessionProperty(stateHolderKey, stateHolder); + System.out.println("[REALTIME] ✓ State holder initialized"); + } + + // Execute backend scanners + System.out.println("[REALTIME] [STEP 3/5] Creating ScanManager..."); + com.checkmarx.eclipse.devassist.basescanner.ScanManager scanManager = + new com.checkmarx.eclipse.devassist.basescanner.ScanManager(registry, stateHolder); + + String filePath = file.getLocation().toOSString(); + System.out.println("[REALTIME] [STEP 4/5] Executing backend scanners for: " + filePath); + + java.util.List issues = + scanManager.scanFile(filePath); + + System.out.println("[REALTIME] ✓ Scan completed - found " + issues.size() + " issues"); + for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : issues) { + System.out.println("[REALTIME] - " + issue.getScanEngine() + ": " + issue.getTitle() + + " (severity: " + issue.getSeverity() + ")"); + } + + // Publish results to UI + System.out.println("[REALTIME] [STEP 5/5] Publishing results to UI..."); + if (!issues.isEmpty()) { + com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues); + System.out.println("[REALTIME] ✓ Results successfully published to findings view"); + } else { + System.out.println("[REALTIME] ℹ️ No issues found - findings view will be empty for this file"); + } + + } 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); + } + } + + System.out.println("[REALTIME] ════════════════════════════════════════"); + return Status.OK_STATUS; + + } catch (Exception e) { + System.err.println("[REALTIME] ✗ UNEXPECTED ERROR during real-time scan: " + e.getMessage()); + e.printStackTrace(); + System.err.println("[REALTIME] Full stack trace:"); + for (StackTraceElement elem : e.getStackTrace()) { + System.err.println("[REALTIME] at " + elem); + } + // Return error status but don't fail the job permanently + return new Status(IStatus.WARNING, "com.checkmarx.eclipse.plugin", + "Real-time scan failed for " + fileName, e); + } + } + + /** + * Check if user is authenticated by checking if API key is configured. + */ + private boolean isUserAuthenticated() { + String apiKey = com.checkmarx.eclipse.properties.Preferences.getApiKey(); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + @Override + public boolean belongsTo(Object family) { + // Group all Checkmarx real-time scan jobs together + // This allows Eclipse to cancel all scans at once if needed + return family != null && family.equals("com.checkmarx.realtime.scan"); + } + + /** + * Called when the job is cancelled. + * Cleanup any resources if needed. + */ + @Override + protected void canceling() { + System.out.println("[REALTIME] Cancelling scan for: " + fileName); + super.canceling(); + } + + public String getFileName() { + return fileName; + } + + public IFile getFile() { + return file; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java new file mode 100644 index 00000000..31584f54 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/CheckmarxMarkerResolutionGenerator.java @@ -0,0 +1,26 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolutionGenerator2; + +/** + * Provides marker resolutions for Checkmarx findings. + * Invoked when user presses Ctrl+1 on a marker or selects "Quick Fix" from context menu. + * Implements IMarkerResolutionGenerator2 for efficient hasResolutions() check. + */ +public class CheckmarxMarkerResolutionGenerator implements IMarkerResolutionGenerator2 { + + @Override + public IMarkerResolution[] getResolutions(IMarker marker) { + return new IMarkerResolution[] { + new ViewFindingDetailsResolution(marker) + }; + } + + @Override + public boolean hasResolutions(IMarker marker) { + // We always provide the "View Finding Details" resolution + return true; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java new file mode 100644 index 00000000..145cd157 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java @@ -0,0 +1,258 @@ +package com.checkmarx.eclipse.devassist.ui.findings.resolution; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.PlatformUI; + +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.model.ScanIssue; + +/** + * Marker resolution that opens a dialog showing complete finding details. + * Reconstructs ScanIssue from marker attributes and displays rich UI. + * Implements IMarkerResolution2 for better performance with hasResolutions() check. + */ +public class ViewFindingDetailsResolution implements IMarkerResolution2 { + + public ViewFindingDetailsResolution(IMarker marker) { + // Constructor parameter kept for instantiation, marker details retrieved from run() parameter + } + + @Override + public String getLabel() { + return "View Finding Details"; + } + + @Override + public String getDescription() { + return "Open detailed information about this Checkmarx finding"; + } + + @Override + public Image getImage() { + // Optional: Return an icon. For now, use default + return null; + } + + @Override + public void run(IMarker marker) { + try { + // Reconstruct ScanIssue from marker attributes + ScanIssue issue = MarkerIssueMapper.fromMarker(marker); + if (issue == null) { + System.out.println("[CX-RESOLUTION] Failed to reconstruct ScanIssue from marker"); + return; + } + + // Open the details dialog + FindingDetailsDialog dialog = new FindingDetailsDialog( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), + issue + ); + dialog.open(); + + System.out.println("[CX-RESOLUTION] Opened finding details: " + issue.getTitle()); + + } catch (Exception e) { + System.out.println("[CX-RESOLUTION] Error opening finding details: " + e.getMessage()); + 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() { + System.out.println("[FINDING-DETAILS] Quick Fix clicked for: " + issue.getTitle()); + // TODO: Implement remediation integration + } + + private void onIgnoreClick() { + System.out.println("[FINDING-DETAILS] Ignore clicked for: " + issue.getTitle()); + // 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(); + System.out.println("[FINDING-DETAILS] ✓ Copied to clipboard"); + }); + } + + private void onOpenWindowClick() { + System.out.println("[FINDING-DETAILS] Open Findings Window clicked for: " + issue.getTitle()); + // TODO: Open Findings window and navigate to this issue + } + + private String getSeverityIcon(String severity) { + if (severity == null) { + return "⚪"; + } + switch (severity.toLowerCase()) { + case "critical": + return "🔴"; + case "high": + return "🟠"; + case "medium": + return "🟡"; + case "low": + return "🟢"; + default: + return "⚪"; + } + } + + private String getSeverityText(String severity) { + return severity != null ? severity.toUpperCase() : "UNKNOWN"; + } + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java new file mode 100644 index 00000000..486a986e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/utils/FindingsUtils.java @@ -0,0 +1,94 @@ +package com.checkmarx.eclipse.devassist.ui.findings.utils; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +import java.util.Arrays; +import java.util.List; + +/** + * Utility methods for findings view. + */ +public class FindingsUtils { + + private static final List SEVERITY_ORDER = Arrays.asList( + "malicious", "critical", "high", "medium", "low"); + + /** + * Check if a severity level represents a problem. + * + * @param severity Severity level + * @return true if severity is a problem level + */ + public static boolean isProblem(String severity) { + if (severity == null) { + return false; + } + String lower = severity.toLowerCase(); + return lower.equals("malicious") || lower.equals("critical") + || lower.equals("high") || lower.equals("medium") || lower.equals("low"); + } + + /** + * Get severity order priority (lower number = higher severity). + * + * @param severity Severity level + * @return Priority index (0 = highest) + */ + public static int getSeverityPriority(String severity) { + if (severity == null) { + return Integer.MAX_VALUE; + } + int index = SEVERITY_ORDER.indexOf(severity.toLowerCase()); + return index >= 0 ? index : Integer.MAX_VALUE; + } + + /** + * Get formatted issue text based on scan engine type. + * + * @param issue Scan issue + * @return Formatted text + */ + public static String getFormattedIssueText(ScanIssue issue) { + if (issue == null) { + return ""; + } + + ScanEngine engine = issue.getScanEngine(); + if (engine == null) { + return issue.getDescription(); + } + + switch (engine) { + case OSS: + return issue.getSeverity() + "-risk package: " + issue.getTitle() + "@" + issue.getPackageVersion(); + case SECRETS: + return issue.getSeverity() + "-risk secret: " + issue.getTitle(); + case CONTAINERS: + return issue.getSeverity() + "-risk container image: " + issue.getTitle() + ":" + issue.getImageTag(); + case ASCA: + case IAC: + return issue.getTitle(); + default: + return issue.getDescription(); + } + } + + /** + * Extract file name from full path. + * + * @param filePath Full file path + * @return File name + */ + public static String getFileName(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return "Unknown"; + } + int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + if (lastSeparator >= 0) { + return filePath.substring(lastSeparator + 1); + } + return filePath; + } +} + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java index e520f992..49a9e276 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java @@ -7,10 +7,18 @@ import org.eclipse.ui.PlatformUI; import com.checkmarx.eclipse.utils.CxLogger; +import com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxEditorListener; +import com.checkmarx.eclipse.devassist.ui.findings.realtime.FindingsEditorHoverListener; +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.listener.ProjectLifecycleListener; public class PluginStartup implements IStartup { private static final String VIEW_ID = "com.checkmarx.eclipse.views.CheckmarxView"; + private static final String FINDINGS_VIEW_ID = "com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView"; + private static FindingsEditorHoverListener hoverListener; // Keep strong reference to prevent GC + private static CheckmarxEditorListener realtimeScanListener; // Keep strong reference to prevent GC + private static ProjectLifecycleListener projectListener; // Keep strong reference to prevent GC @Override public void earlyStartup() { @@ -19,13 +27,54 @@ 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 custom hover on findings annotations + hoverListener = new FindingsEditorHoverListener(); + window.getPartService().addPartListener(hoverListener); + + // Register listener for real-time scanning with debounce + realtimeScanListener = new CheckmarxEditorListener(); + window.getPartService().addPartListener(realtimeScanListener); + + // Initialize backend scanner infrastructure + initializeBackendScanners(); } } catch (PartInitException e) { - CxLogger.error("Failed to open Checkmarx One view on startup: " + e.getMessage(), e); + CxLogger.error("Failed to open Checkmarx views on startup: " + e.getMessage(), e); + } catch (Exception e) { + CxLogger.error("Error during plugin startup: " + e.getMessage(), e); } }); } + + /** + * Initialize backend scanner infrastructure. + * + * Creates and registers: + * - GlobalScannerController (application-level singleton) + * - ProjectLifecycleListener (project open/close listener) + * + * This enables real-time scanning on file modifications. + */ + private void initializeBackendScanners() { + try { + GlobalScannerController controller = GlobalScannerController.getInstance(); + CxLogger.info(controller.getStateReport()); + + projectListener = new ProjectLifecycleListener(); + projectListener.register(); + } catch (Exception e) { + CxLogger.error("Error initializing backend scanners: " + e.getMessage(), e); + } + } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java index 524b1136..6a37b498 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginConstants.java @@ -21,6 +21,7 @@ public class PluginConstants { public static final String BFL_NOT_FOUND = "Best fix Location not available for given results"; public static final String TOOLBAR_ACTION_PREFERENCES = "Preferences"; public static final String TOOLBAR_ACTION_CLEAR_RESULTS = "Clear results section"; + public static final String FINDINGS_PROMO_DESCRIPTION = "Checkmarx AI (Cx Assist) provides real-time threat detection and helps you avoid vulnerabilities before they happen."; /******************************** LOG VIEW: ERRORS ********************************/ diff --git a/pom.xml b/pom.xml index ddc93a88..a1a45693 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,8 @@ + + From 228d0143c6d2c684875551b853081d65dccea575 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Mon, 3 Aug 2026 12:42:05 +0530 Subject: [PATCH 2/9] Removed unecessay code --- checkmarx-ast-eclipse-plugin/plugin.xml | 30 -- .../backend/GlobalScannerController.java | 101 ------ .../prefs/CheckmarxPreferencePage.java | 214 ------------ .../devassist/state/ScanFrequency.java | 36 -- .../eclipse/devassist/state/ScannerState.java | 46 --- .../devassist/state/ScannerStateManager.java | 73 ---- .../ui/findings/editor/CxFindingsHover.java | 194 ----------- .../editor/CxFindingsHoverControl.java | 254 -------------- .../editor/CxFindingsInformationControl.java | 47 --- .../editor/FindingsHoverProvider.java | 321 ------------------ .../ignored/CxIgnoredProblemsView.java | 269 --------------- .../IgnoredProblemsContentProvider.java | 78 ----- .../ignored/IgnoredProblemsLabelProvider.java | 76 ----- .../realtime/FindingsEditorHoverListener.java | 92 ----- .../eclipse/startup/PluginStartup.java | 6 - 15 files changed, 1837 deletions(-) delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java diff --git a/checkmarx-ast-eclipse-plugin/plugin.xml b/checkmarx-ast-eclipse-plugin/plugin.xml index 676e436b..10f2d0e1 100644 --- a/checkmarx-ast-eclipse-plugin/plugin.xml +++ b/checkmarx-ast-eclipse-plugin/plugin.xml @@ -9,11 +9,6 @@ id="com.checkmarx.eclipse.properties.preferencespage" name="Checkmarx One"> - - @@ -35,15 +30,6 @@ name="Checkmarx One Assist Findings" restorable="true"> - - @@ -65,12 +51,6 @@ relationship="stacked" ratio="0.5"> - - @@ -190,14 +170,4 @@ - - - - - - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java index 0801e9ac..ab971710 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java @@ -33,18 +33,6 @@ public class GlobalScannerController { // Listeners notified when scanner state changes private final List stateListeners = new ArrayList<>(); - // State manager for persistence - private final com.checkmarx.eclipse.devassist.state.ScannerStateManager stateManager = - new com.checkmarx.eclipse.devassist.state.ScannerStateManager(); - - // Preference change listener to reload state when preferences are saved - private final org.eclipse.jface.util.IPropertyChangeListener prefChangeListener = - event -> { - if ("scannerPreferencesChanged".equals(event.getProperty())) { - reloadStateFromPreferences(); - } - }; - /** * Get the global singleton instance. * Lazily creates on first access. @@ -54,70 +42,10 @@ public class GlobalScannerController { public synchronized static GlobalScannerController getInstance() { if (instance == null) { instance = new GlobalScannerController(); - instance.initializeDefaults(); - instance.registerPreferenceListener(); } return instance; } - /** - * Initialize scanner state from preferences. - */ - private void initializeDefaults() { - CxLogger.info(LOG_TAG + " Initializing scanner state from preferences"); - - com.checkmarx.eclipse.devassist.state.ScannerState state = stateManager.loadState(); - - for (ScannerType type : ScannerType.values()) { - com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); - if (engine != null) { - boolean enabled = state.isEnabled(engine); - scannerState.put(type, enabled); - String status = enabled ? "enabled" : "disabled"; - CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " " + status); - } - } - } - - /** - * Register listener for preference changes. - * When preferences are saved, reload scanner state from preferences. - */ - private void registerPreferenceListener() { - try { - com.checkmarx.eclipse.Activator.getDefault().getPreferenceStore() - .addPropertyChangeListener(prefChangeListener); - CxLogger.info(LOG_TAG + " Preference listener registered"); - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Failed to register preference listener: " + e.getMessage()); - } - } - - /** - * Reload scanner state from preferences. - * Called when preferences are saved to pick up any changes. - */ - private void reloadStateFromPreferences() { - CxLogger.info(LOG_TAG + " Reloading scanner state from preferences"); - - com.checkmarx.eclipse.devassist.state.ScannerState state = stateManager.loadState(); - - for (ScannerType type : ScannerType.values()) { - com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); - if (engine != null) { - boolean enabled = state.isEnabled(engine); - boolean wasEnabled = scannerState.getOrDefault(type, true); - - if (enabled != wasEnabled) { - scannerState.put(type, enabled); - String status = enabled ? "enabled" : "disabled"; - CxLogger.info(LOG_TAG + " Updated " + type.getDisplayName() + " to " + status); - notifyScannerStateChanged(type, enabled); - } - } - } - } - /** * Enable a scanner globally. * @@ -132,10 +60,6 @@ public void enableScanner(ScannerType type) { if (wasDisabled) { CxLogger.info(LOG_TAG + " Enabled scanner: " + type.getDisplayName()); - com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); - if (engine != null) { - stateManager.setScannerEnabled(engine, true); - } notifyScannerStateChanged(type, true); } } @@ -154,10 +78,6 @@ public void disableScanner(ScannerType type) { if (wasEnabled) { CxLogger.info(LOG_TAG + " Disabled scanner: " + type.getDisplayName()); - com.checkmarx.eclipse.devassist.model.ScanEngine engine = typeToEngine(type); - if (engine != null) { - stateManager.setScannerEnabled(engine, false); - } notifyScannerStateChanged(type, false); } } @@ -273,27 +193,6 @@ public String getStateReport() { return sb.toString(); } - /** - * Convert ScannerType to ScanEngine enum. - * Used for bridging between global controller and state manager. - * - * @param type Scanner type - * @return Corresponding ScanEngine, or null if no mapping exists - */ - private com.checkmarx.eclipse.devassist.model.ScanEngine typeToEngine(ScannerType type) { - if (type == null) { - return null; - } - - return switch (type) { - case ASCA -> com.checkmarx.eclipse.devassist.model.ScanEngine.ASCA; - case OSS -> com.checkmarx.eclipse.devassist.model.ScanEngine.OSS; - case SECRETS -> com.checkmarx.eclipse.devassist.model.ScanEngine.SECRETS; - case IAC -> com.checkmarx.eclipse.devassist.model.ScanEngine.IAC; - case CONTAINERS -> com.checkmarx.eclipse.devassist.model.ScanEngine.CONTAINERS; - }; - } - /** * Listener interface for scanner state changes. * Implemented by project registries to react to global changes. diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java deleted file mode 100644 index e7defebc..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/prefs/CheckmarxPreferencePage.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.checkmarx.eclipse.devassist.prefs; - -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPreferencePage; - -import com.checkmarx.eclipse.Activator; - -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.preference.PreferencePage; -import org.eclipse.swt.SWT; -import org.eclipse.swt.custom.StyleRange; -import org.eclipse.swt.custom.StyledText; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.*; - -/** - * 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 Label ascaInstallationMsg; - private Button ossCheckbox; - private Button secretsCheckbox; - private Button containersCheckbox; - private Button iacCheckbox; - private Combo containersToolCombo; - private Label mcpStatusLabel; - - 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 CheckmarxPreferencePage() { - super(); - setPreferenceStore(Activator.getDefault().getPreferenceStore()); - } - - @Override - protected Control createContents(Composite parent) { - Composite mainPanel = new Composite(parent, SWT.NONE); - GridLayout layout = new GridLayout(1, false); - layout.verticalSpacing = 8; - layout.horizontalSpacing = 0; - mainPanel.setLayout(layout); - mainPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); - - // Assist Message Label (Hidden by default, red text) - assistMessageLabel = new Label(mainPanel, SWT.NONE); - assistMessageLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); - GridData msgData = new GridData(GridData.FILL_HORIZONTAL); - msgData.exclude = true; // Equivalent to hidemode 3 - assistMessageLabel.setLayoutData(msgData); - assistMessageLabel.setVisible(false); - - // --- ASCA Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); - Composite ascaComp = createIndentComposite(mainPanel); - ascaCheckbox = new Button(ascaComp, SWT.CHECK); - ascaCheckbox.setText("Enable ASCA Scanner"); - - // --- OSS Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); - Composite ossComp = createIndentComposite(mainPanel); - ossCheckbox = new Button(ossComp, SWT.CHECK); - ossCheckbox.setText("Enable OSS Scanner"); - - // --- Secrets Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); - Composite secretsComp = createIndentComposite(mainPanel); - secretsCheckbox = new Button(secretsComp, SWT.CHECK); - secretsCheckbox.setText("Enable Secrets Scanner"); - - // --- Containers Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); - Composite containersComp = createIndentComposite(mainPanel); - containersCheckbox = new Button(containersComp, SWT.CHECK); - containersCheckbox.setText("Enable Container Scanner"); - - // --- IaC Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); - Composite iacComp = createIndentComposite(mainPanel); - iacCheckbox = new Button(iacComp, SWT.CHECK); - iacCheckbox.setText("Enable IaC Scanner"); - - // --- Container Tool Selection Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); - Composite containerToolComp = createIndentComposite(mainPanel); - Label containerDesc = new Label(containerToolComp, SWT.WRAP); - containerDesc.setText(CONTAINERS_TOOL_DESCRIPTION); - GridData descData = new GridData(GridData.FILL_HORIZONTAL); - containerDesc.setLayoutData(descData); - - containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); - containersToolCombo.setItems(new String[] { "docker", "podman"}); - containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); - - loadValues(); - return mainPanel; - } - - 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() { - 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() { - // Save current UI control state into PreferenceStore - IPreferenceStore store = getPreferenceStore(); - store.setValue(PREF_ASCA_ENABLED, ascaCheckbox.getSelection()); - store.setValue(PREF_OSS_ENABLED, ossCheckbox.getSelection()); - store.setValue(PREF_SECRETS_ENABLED, secretsCheckbox.getSelection()); - store.setValue(PREF_CONTAINERS_ENABLED, containersCheckbox.getSelection()); - store.setValue(PREF_IAC_ENABLED, iacCheckbox.getSelection()); - - if (containersToolCombo.getText() != null) { - store.setValue(PREF_CONTAINERS_TOOL, containersToolCombo.getText()); - } - - // Trigger change event for listeners - store.firePropertyChangeEvent("scannerPreferencesChanged", null, null); - - return super.performOk(); - } - -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java deleted file mode 100644 index b32d517a..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScanFrequency.java +++ /dev/null @@ -1,36 +0,0 @@ -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/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java deleted file mode 100644 index 74cbd7ea..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerState.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.checkmarx.eclipse.devassist.state; - -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import java.util.HashMap; -import java.util.Map; - -/** - * Represents the current state of scanner enable/disable settings. - * Holds which scanners are enabled and scan frequency preference. - */ -public class ScannerState { - - private final Map scannerStates = new HashMap<>(); - private ScanFrequency frequency; - - public ScannerState() { - initializeDefaults(); - } - - private void initializeDefaults() { - for (ScanEngine engine : ScanEngine.values()) { - scannerStates.put(engine, true); - } - this.frequency = ScanFrequency.ON_DOCUMENT_CHANGE; - } - - public boolean isEnabled(ScanEngine engine) { - return scannerStates.getOrDefault(engine, true); - } - - public void setEnabled(ScanEngine engine, boolean enabled) { - scannerStates.put(engine, enabled); - } - - public ScanFrequency getFrequency() { - return frequency; - } - - public void setFrequency(ScanFrequency frequency) { - this.frequency = frequency; - } - - public Map getAllStates() { - return new HashMap<>(scannerStates); - } -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java deleted file mode 100644 index f3493161..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.checkmarx.eclipse.devassist.state; - -import org.eclipse.jface.preference.IPreferenceStore; - -import com.checkmarx.eclipse.Activator; -import com.checkmarx.eclipse.devassist.model.ScanEngine; - -/** - * Manages scanner state persistence using Eclipse preferences. - * Loads and saves which scanners are enabled/disabled and scan frequency preference. - */ -public class ScannerStateManager { - - private static final String KEY_PREFIX = "scanner."; - private static final String KEY_ENABLED_SUFFIX = ".enabled"; - private static final String KEY_FREQUENCY = "scan.frequency"; - - private final IPreferenceStore prefs; - - public ScannerStateManager() { - this.prefs = Activator.getDefault().getPreferenceStore(); - } - - public ScannerStateManager(IPreferenceStore prefs) { - this.prefs = prefs; - } - - public ScannerState loadState() { - ScannerState state = new ScannerState(); - - for (ScanEngine engine : ScanEngine.values()) { - String key = getEnabledKey(engine); - boolean enabled = prefs.getBoolean(key); - state.setEnabled(engine, enabled); - } - - String freqKey = prefs.getString(KEY_FREQUENCY); - state.setFrequency(ScanFrequency.fromKey(freqKey)); - - return state; - } - - public void saveState(ScannerState state) { - for (ScanEngine engine : ScanEngine.values()) { - String key = getEnabledKey(engine); - boolean enabled = state.isEnabled(engine); - prefs.setValue(key, enabled); - } - - prefs.setValue(KEY_FREQUENCY, state.getFrequency().getKey()); - } - - public boolean isScannerEnabled(ScanEngine engine) { - return prefs.getBoolean(getEnabledKey(engine)); - } - - public void setScannerEnabled(ScanEngine engine, boolean enabled) { - prefs.setValue(getEnabledKey(engine), enabled); - } - - public ScanFrequency getScanFrequency() { - String key = prefs.getString(KEY_FREQUENCY); - return ScanFrequency.fromKey(key); - } - - public void setScanFrequency(ScanFrequency frequency) { - prefs.setValue(KEY_FREQUENCY, frequency.getKey()); - } - - private String getEnabledKey(ScanEngine engine) { - return KEY_PREFIX + engine.name().toLowerCase() + KEY_ENABLED_SUFFIX; - } -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java deleted file mode 100644 index 27a375ba..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHover.java +++ /dev/null @@ -1,194 +0,0 @@ -//package com.checkmarx.eclipse.devassist.ui.findings.editor; -// -//import org.eclipse.jface.text.IInformationControl; -//import org.eclipse.jface.text.IInformationControlCreator; -//import org.eclipse.jface.text.IRegion; -//import org.eclipse.jface.text.ITextHover; -//import org.eclipse.jface.text.ITextHoverExtension; -//import org.eclipse.jface.text.ITextHoverExtension2; -//import org.eclipse.jface.text.ITextViewer; -//import org.eclipse.jface.text.Region; -//import org.eclipse.jface.text.source.Annotation; -//import org.eclipse.jface.text.source.IAnnotationModel; -//import org.eclipse.ui.IEditorPart; -//import org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover; -// -///** -// * Custom hover for Checkmarx Findings annotations in the editor. -// * -// * Finds FindingsAnnotation objects at the hover offset and displays -// * detailed vulnerability information via CxFindingsHoverControl. -// * -// * Works independently of Eclipse markers - uses ScanIssue annotation model. -// */ -//public class CxFindingsHover implements IJavaEditorTextHover, ITextHover, ITextHoverExtension, ITextHoverExtension2 { -// -// private FindingsAnnotation currentAnnotation; -// public CxFindingsHover() { -// // Default constructor for Eclipse instantiation -// } -// -// @Override -// public void setEditor(IEditorPart editor) { -// } -// -// @Override -// public String getHoverInfo(ITextViewer viewer, IRegion hoverRegion) { -// return null; -// } -// -// @Override -// public Object getHoverInfo2(ITextViewer viewer, IRegion hoverRegion) { -// System.out.println("[CX-FINDINGS-HOVER] getHoverInfo2 called"); -// return this.currentAnnotation; -// } -// -// @Override -// public IInformationControlCreator getHoverControlCreator() { -// return new IInformationControlCreator() { -// @Override -// public IInformationControl createInformationControl(org.eclipse.swt.widgets.Shell parent) { -// System.out.println("[CX-FINDINGS-HOVER] Creating hover control for annotation: " + currentAnnotation); -// if (currentAnnotation != null) { -// return new CxFindingsHoverControl(parent, currentAnnotation); -// } -// return null; -// } -// }; -// } -// -// @Override -// public IRegion getHoverRegion(ITextViewer viewer, int offset) { -// System.out.println("[CX-FINDINGS-HOVER] getHoverRegion called at offset: " + offset); -// try { -// // Find FindingsAnnotation at this offset -// FindingsAnnotation annotation = findAnnotationContainingOffset(viewer, offset); -// if (annotation == null) { -// System.out.println("[CX-FINDINGS-HOVER] No annotation found at offset " + offset); -// return null; -// } -// -// // Cache the annotation for getHoverInfo2() -// this.currentAnnotation = annotation; -// System.out.println("[CX-FINDINGS-HOVER] ✓ Found annotation: " + annotation.getTitle()); -// -// // Return the region covered by the annotation in the annotation model -// if (viewer instanceof org.eclipse.jface.text.source.ISourceViewer) { -// org.eclipse.jface.text.source.ISourceViewer sourceViewer = -// (org.eclipse.jface.text.source.ISourceViewer) viewer; -// IAnnotationModel annotationModel = sourceViewer.getAnnotationModel(); -// if (annotationModel != null) { -// org.eclipse.jface.text.Position pos = annotationModel.getPosition(annotation); -// if (pos != null) { -// System.out.println("[CX-FINDINGS-HOVER] ✓ Returning region: " + pos.getOffset() + "-" + (pos.getOffset() + pos.getLength())); -// return new Region(pos.getOffset(), pos.getLength()); -// } -// } -// } -// } catch (Exception e) { -// System.err.println("[CX-FINDINGS-HOVER] Error in getHoverRegion: " + e.getMessage()); -// } -// return null; -// } -// -// /** -// * Find a FindingsAnnotation whose position contains the given offset. -// * If multiple annotations overlap, returns the innermost (smallest range). -// */ -// private FindingsAnnotation findAnnotationContainingOffset(ITextViewer viewer, int offset) { -// try { -// if (viewer == null) { -// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: viewer is null"); -// return null; -// } -// -// IAnnotationModel annotationModel = null; -// if (viewer instanceof org.eclipse.jface.text.source.ISourceViewer) { -// annotationModel = ((org.eclipse.jface.text.source.ISourceViewer) viewer).getAnnotationModel(); -// } -// -// if (annotationModel == null) { -// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: annotation model is null"); -// return null; -// } -// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: Searching annotations..."); -// -// FindingsAnnotation bestAnnotation = null; -// int smallestRange = Integer.MAX_VALUE; -// -// // Iterate through all annotations in the model -// @SuppressWarnings("unchecked") -// java.util.Iterator iterator = annotationModel.getAnnotationIterator(); -// while (iterator.hasNext()) { -// Annotation annotation = iterator.next(); -// -// if (annotation instanceof FindingsAnnotation) { -// FindingsAnnotation findingsAnnotation = (FindingsAnnotation) annotation; -// org.eclipse.jface.text.Position pos = annotationModel.getPosition(annotation); -// -// if (pos != null) { -// int start = pos.getOffset(); -// int end = pos.getOffset() + pos.getLength(); -// -// System.out.println("[CX-FINDINGS-HOVER] Annotation: " + findingsAnnotation.getTitle() + -// " range=[" + start + "-" + end + "]"); -// -// // Check if offset falls within this annotation's range -// if (start <= offset && offset < end) { -// int range = pos.getLength(); -// System.out.println("[CX-FINDINGS-HOVER] ✓ Offset " + offset + " is INSIDE range, size=" + range); -// if (range < smallestRange) { -// smallestRange = range; -// bestAnnotation = findingsAnnotation; -// System.out.println("[CX-FINDINGS-HOVER] ✓ Selected as best annotation (innermost)"); -// } -// } else { -// System.out.println("[CX-FINDINGS-HOVER] ✗ Offset " + offset + " is OUTSIDE range"); -// } -// } -// } -// } -// -// if (bestAnnotation != null) { -// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: ✓ FOUND annotation"); -// } else { -// System.out.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: ✗ NO annotation found"); -// } -// -// return bestAnnotation; -// } catch (Exception e) { -// System.err.println("[CX-FINDINGS-HOVER] findAnnotationContainingOffset: EXCEPTION - " + e.getMessage()); -// e.printStackTrace(); -// return null; -// } -// } -//} - -package com.checkmarx.eclipse.devassist.ui.findings.editor; - -import org.eclipse.jface.text.*; -import org.eclipse.swt.widgets.Shell; - -public class CxFindingsHover implements ITextHover, ITextHoverExtension { - - @Override - public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { - return null; // Not used when ITextHoverExtension is implemented - } - - @Override - public IRegion getHoverRegion(ITextViewer textViewer, int offset) { - return new Region(offset, 0); - } - - @Override - public IInformationControlCreator getHoverControlCreator() { - return new AbstractReusableInformationControlCreator() { - @Override - protected IInformationControl doCreateInformationControl(Shell parent) { - // Returns custom popup window containing real SWT Buttons - return new CxFindingsInformationControl(parent); - } - }; - } -} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java deleted file mode 100644 index 86a98aaf..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsHoverControl.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.editor; - -import org.eclipse.jface.text.AbstractInformationControl; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.graphics.Color; -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.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; -import java.util.Timer; -import java.util.TimerTask; - -/** - * Custom hover control for Checkmarx Findings. - * - * Displays: - * - Issue severity badge with color - * - Issue title/message - * - Line number - * - Action buttons (Ignore, View Details, etc.) - * - * Includes auto-close timer that pauses on mouse hover. - */ -public class CxFindingsHoverControl extends AbstractInformationControl { - - private FindingsAnnotation annotation; - private Composite mainComposite; - private Timer closeTimer; - - public CxFindingsHoverControl(Shell parent, FindingsAnnotation annotation) { - super(parent, true); - this.annotation = annotation; - System.out.println("[FINDINGS-HOVER] CxFindingsHoverControl created"); - create(); - } - - @Override - public boolean hasContents() { - return annotation != null && annotation.getTitle() != null; - } - - @Override - protected void createContent(Composite parent) { - System.out.println("[FINDINGS-HOVER] Creating hover content..."); - - mainComposite = new Composite(parent, SWT.NONE); - GridLayout layout = new GridLayout(1, false); - layout.marginHeight = 10; - layout.marginWidth = 10; - layout.verticalSpacing = 8; - mainComposite.setLayout(layout); - mainComposite.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); - - try { - // 1. Severity Badge - createSeverityBadge(); - - // 2. Title/Message - createMessageSection(); - - // 3. Separator - Label separator = new Label(mainComposite, SWT.SEPARATOR | SWT.HORIZONTAL); - separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - - // 4. Action Buttons - createActionButtonsSection(); - - parent.layout(); - - // Add mouse tracking to keep popup open when hovering - addMouseTrackingToAllChildren(mainComposite); - addMouseTrackingToAllChildren(parent); - - System.out.println("[FINDINGS-HOVER] ✓ Content created successfully"); - - } catch (Exception e) { - System.err.println("[FINDINGS-HOVER] Error creating content: " + e.getMessage()); - e.printStackTrace(); - } - } - - private void addMouseTrackingToAllChildren(Composite composite) { - if (composite == null || composite.isDisposed()) return; - - composite.addMouseTrackListener(new org.eclipse.swt.events.MouseTrackListener() { - @Override - public void mouseEnter(org.eclipse.swt.events.MouseEvent e) { - System.out.println("[FINDINGS-HOVER] Mouse ENTERED control"); - if (closeTimer != null) { - closeTimer.cancel(); - closeTimer = null; - } - } - - @Override - public void mouseExit(org.eclipse.swt.events.MouseEvent e) { - System.out.println("[FINDINGS-HOVER] Mouse EXITED control - starting close timer"); - if (closeTimer != null) { - closeTimer.cancel(); - } - closeTimer = new Timer(); - closeTimer.schedule(new TimerTask() { - @Override - public void run() { - Display.getDefault().asyncExec(() -> { - try { - CxFindingsHoverControl.super.dispose(); - } catch (Exception ex) { - // Already disposed - } - }); - } - }, 500); - } - - @Override - public void mouseHover(org.eclipse.swt.events.MouseEvent e) { - // Not needed - } - }); - - // Recursively add to all children - for (org.eclipse.swt.widgets.Control child : composite.getChildren()) { - if (child instanceof Composite) { - addMouseTrackingToAllChildren((Composite) child); - } - } - } - - /** - * Create severity badge with color - */ - private void createSeverityBadge() { - Composite severityComposite = new Composite(mainComposite, SWT.NONE); - severityComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - GridLayout severityLayout = new GridLayout(2, false); - severityLayout.marginHeight = 6; - severityLayout.marginWidth = 8; - severityLayout.verticalSpacing = 0; - severityLayout.horizontalSpacing = 8; - severityComposite.setLayout(severityLayout); - severityComposite.setBackground(getSeverityColor()); - - Label severityIcon = new Label(severityComposite, SWT.NONE); - severityIcon.setText(getSeverityIcon()); - severityIcon.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); - severityIcon.setFont(mainComposite.getFont()); - - Label severityLabel = new Label(severityComposite, SWT.NONE); - severityLabel.setText("CHECKMARX FINDING"); - severityLabel.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); - severityLabel.setFont(mainComposite.getFont()); - } - - /** - * Create message/title section - */ - private void createMessageSection() { - Label messageLabel = new Label(mainComposite, SWT.WRAP); - messageLabel.setText(annotation.getTitle()); - messageLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); - messageLabel.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); - - // ✓ Use Browser for HTML descriptions (with buttons) - if (annotation.getDescription() != null && !annotation.getDescription().isEmpty()) { - org.eclipse.swt.browser.Browser browser = new org.eclipse.swt.browser.Browser(mainComposite, SWT.NONE); - browser.setText(annotation.getDescription()); // ← Now renders HTML properly - GridData browserData = new GridData(SWT.FILL, SWT.FILL, true, true); - browserData.widthHint = 400; - browserData.heightHint = 120; - browser.setLayoutData(browserData); - } - } - - /** - * Create action buttons - */ - private void createActionButtonsSection() { - Composite buttonComposite = new Composite(mainComposite, SWT.NONE); - buttonComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - GridLayout buttonLayout = new GridLayout(2, true); - buttonLayout.marginHeight = 4; - buttonLayout.marginWidth = 0; - buttonLayout.horizontalSpacing = 6; - buttonComposite.setLayout(buttonLayout); - - // Ignore Button - Button ignoreBtn = new Button(buttonComposite, 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) { - System.out.println("[FINDINGS-HOVER] Ignore clicked for: " + annotation.getTitle()); - dispose(); - } - }); - - // Details Button - Button detailsBtn = new Button(buttonComposite, SWT.PUSH); - detailsBtn.setText("Details"); - detailsBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - detailsBtn.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - System.out.println("[FINDINGS-HOVER] Details clicked for: " + annotation.getTitle()); - dispose(); - } - }); - } - - /** - * Get severity color from annotation type - */ - private Color getSeverityColor() { - Display display = Display.getCurrent(); - String annotationType = annotation.getType(); - - if (annotationType != null) { - if (annotationType.contains("critical")) { - return display.getSystemColor(SWT.COLOR_RED); - } else if (annotationType.contains("high")) { - return display.getSystemColor(SWT.COLOR_DARK_RED); - } else if (annotationType.contains("medium")) { - return display.getSystemColor(SWT.COLOR_DARK_YELLOW); - } - } - - return display.getSystemColor(SWT.COLOR_DARK_BLUE); - } - - /** - * Get severity icon emoji - */ - private String getSeverityIcon() { - String annotationType = annotation.getType(); - if (annotationType != null) { - if (annotationType.contains("critical")) { - return "🔴"; - } else if (annotationType.contains("high")) { - return "🟠"; - } else if (annotationType.contains("medium")) { - return "🟡"; - } - } - return "🔵"; - } -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java deleted file mode 100644 index 4a2c2dae..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/CxFindingsInformationControl.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.editor; - -import org.eclipse.jface.text.AbstractInformationControl; -import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.widgets.*; - -public class CxFindingsInformationControl extends AbstractInformationControl { - - public CxFindingsInformationControl(Shell parentShell) { - super(parentShell, false); - create(); - } - - @Override - protected void createContent(Composite parent) { - Composite composite = new Composite(parent, SWT.NONE); - composite.setLayout(new GridLayout(4, true)); - - // Label - Label title = new Label(composite, SWT.NONE); - title.setText("Checkmarx Finding Detected"); - GridData gd = new GridData(SWT.FILL, SWT.CENTER, true, false, 4, 1); - title.setLayoutData(gd); - - // Clickable SWT Buttons inside Hover Popup - Button quickFixBtn = new Button(composite, SWT.PUSH); - quickFixBtn.setText("⚡ Quick Fix"); - quickFixBtn.addListener(SWT.Selection, e -> System.out.println("Quick Fix clicked from hover!")); - - Button ignoreBtn = new Button(composite, SWT.PUSH); - ignoreBtn.setText("🚫 Ignore"); - ignoreBtn.addListener(SWT.Selection, e -> System.out.println("Ignore clicked from hover!")); - - Button copyBtn = new Button(composite, SWT.PUSH); - copyBtn.setText("📋 Copy"); - - Button detailsBtn = new Button(composite, SWT.PUSH); - detailsBtn.setText("🪟 Details"); - } - - @Override - public boolean hasContents() { - return true; - } -} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java deleted file mode 100644 index 111c1cdd..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsHoverProvider.java +++ /dev/null @@ -1,321 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.editor; - -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IInformationControl; -import org.eclipse.jface.text.IInformationControlCreator; -import org.eclipse.jface.text.IRegion; -import org.eclipse.jface.text.ITextHover; -import org.eclipse.jface.text.ITextHoverExtension; -import org.eclipse.jface.text.ITextViewer; -import org.eclipse.jface.text.Region; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.FocusListener; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.swt.widgets.Button; -import org.eclipse.swt.widgets.Text; - -import com.checkmarx.eclipse.devassist.model.ScanIssue; - -/** - * Provides custom hover information for findings in the editor. - * Shows issue details and action buttons on hover. - */ -public class FindingsHoverProvider implements ITextHover, ITextHoverExtension { - - private ScanIssue currentIssue; - private ITextViewer textViewer; - - public FindingsHoverProvider(ScanIssue issue, ITextViewer viewer) { - this.currentIssue = issue; - this.textViewer = viewer; - } - - @Override - public String getHoverInfo(ITextViewer viewer, IRegion hoverRegion) { - System.out.println("[HOVER] getHoverInfo()"); - if (currentIssue == null) { - return null; - } - - // Build tooltip text (used as cache key by the hover framework; actual UI comes from - // FindingsInformationControl, built from currentIssue directly). - StringBuilder tooltip = new StringBuilder(); - String severity = currentIssue.getSeverity(); - tooltip.append("=== ").append(severity != null ? severity.toUpperCase() : "UNKNOWN").append(" ===\n\n"); - tooltip.append("Title: ").append(currentIssue.getTitle()).append("\n\n"); - tooltip.append("Description:\n").append(currentIssue.getDescription()).append("\n\n"); - if (currentIssue.getRemediationAdvise() != null) { - tooltip.append("Remediation:\n").append(currentIssue.getRemediationAdvise()).append("\n\n"); - } - tooltip.append("[This is a custom hover with action buttons below]\n"); - tooltip.append(" [Quick Fix] [Ignore] [Copy] [Open Details]"); - - return tooltip.toString(); - } - - @Override - public IRegion getHoverRegion(ITextViewer viewer, int offset) { - - System.out.println("[HOVER] getHoverRegion offset=" + offset); - // Return region covering the entire line if it's the problematic line - try { - if (viewer == null || currentIssue == null - || currentIssue.getLocations() == null || currentIssue.getLocations().isEmpty()) { - return null; - } - - IDocument document = viewer.getDocument(); - if (document == null) { - return null; - } - - int lineNumber = document.getLineOfOffset(offset); - int problematicLine = currentIssue.getLocations().get(0).getLine() - 1; - if (lineNumber == problematicLine) { - int lineStartOffset = document.getLineOffset(lineNumber); - int lineLength = document.getLineLength(lineNumber); - return new Region(lineStartOffset, lineLength); - } - } catch (BadLocationException e) { - // Offset no longer valid (e.g. document changed) - no hover to show - } - return null; - } - - @Override - public IInformationControlCreator getHoverControlCreator() { - return new IInformationControlCreator() { - @Override - public IInformationControl createInformationControl(Shell parent) { - return new FindingsInformationControl(parent, currentIssue); - } - }; - } - - /** - * Custom information control for displaying findings with action buttons. - */ - public static class FindingsInformationControl implements IInformationControl { - - private Shell shell; - private ScanIssue issue; - - public FindingsInformationControl(Shell parent, ScanIssue issue) { - this.issue = issue; - this.shell = new Shell(parent, SWT.TOOL | SWT.ON_TOP); - this.shell.setLayout(new GridLayout(1, false)); - this.shell.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND)); - - createContents(); - } - - private void createContents() { - // Severity label - Label severityLabel = new Label(shell, SWT.NONE); - severityLabel.setText(getSeverityIcon(issue.getSeverity()) + " " + getSeverityText(issue.getSeverity())); - severityLabel.setFont(shell.getDisplay().getSystemFont()); - GridData gd = new GridData(SWT.FILL, SWT.CENTER, true, false); - severityLabel.setLayoutData(gd); - - // Title label - Label titleLabel = new Label(shell, SWT.WRAP); - titleLabel.setText("Title: " + (issue.getTitle() != null ? issue.getTitle() : "")); - gd = new GridData(SWT.FILL, SWT.CENTER, true, false); - gd.widthHint = 400; - titleLabel.setLayoutData(gd); - - // Description text (scrollable) - Text descriptionText = new Text(shell, SWT.WRAP | SWT.READ_ONLY | SWT.V_SCROLL); - descriptionText.setText(issue.getDescription() != null ? issue.getDescription() : ""); - gd = new GridData(SWT.FILL, SWT.FILL, true, true); - gd.heightHint = 90; - gd.widthHint = 400; - descriptionText.setLayoutData(gd); - - // Remediation advice (if available) - if (issue.getRemediationAdvise() != null && !issue.getRemediationAdvise().isEmpty()) { - Label remediationLabel = new Label(shell, SWT.WRAP); - remediationLabel.setText("Remediation: " + issue.getRemediationAdvise()); - gd = new GridData(SWT.FILL, SWT.CENTER, true, false); - gd.widthHint = 400; - remediationLabel.setLayoutData(gd); - } - - // Buttons composite - Composite buttonsComposite = new Composite(shell, 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.addListener(SWT.Selection, 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.addListener(SWT.Selection, 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.addListener(SWT.Selection, e -> onCopyClick()); - - // Open Details button - Button openBtn = new Button(buttonsComposite, SWT.PUSH); - openBtn.setText("Open Window"); - openBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - openBtn.addListener(SWT.Selection, e -> onOpenWindowClick()); - } - - private static String getSeverityIcon(String severity) { - if (severity == null) { - return "⚪"; // white circle for unknown - } - switch (severity.toLowerCase()) { - case "critical": - return "🔴"; - case "high": - return "🟠"; - case "medium": - return "🟡"; - case "low": - return "🟢"; - default: - return "⚪"; - } - } - - private static String getSeverityText(String severity) { - return severity != null ? severity.toUpperCase() : "UNKNOWN"; - } - - private void onQuickFixClick() { - System.out.println("[FINDINGS-HOVER] Quick Fix clicked for: " + issue.getTitle()); - // TODO: Implement quick fix logic - } - - private void onIgnoreClick() { - System.out.println("[FINDINGS-HOVER] Ignore clicked for: " + issue.getTitle()); - // 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; - shell.getDisplay().asyncExec(() -> { - org.eclipse.swt.dnd.Clipboard clipboard = new org.eclipse.swt.dnd.Clipboard(shell.getDisplay()); - org.eclipse.swt.dnd.TextTransfer transfer = org.eclipse.swt.dnd.TextTransfer.getInstance(); - clipboard.setContents(new Object[] { text }, new org.eclipse.swt.dnd.Transfer[] { transfer }); - clipboard.dispose(); - System.out.println("[FINDINGS-HOVER] ✓ Copied to clipboard"); - }); - } - - private void onOpenWindowClick() { - System.out.println("[FINDINGS-HOVER] Open Window clicked for: " + issue.getTitle()); - // TODO: Open Findings window and navigate to issue - } - - @Override - public void setInformation(String information) { - } - - @Override - public void setSize(int width, int height) { - if (shell != null) { - shell.setSize(width, height); - } - } - - @Override - public void setLocation(Point location) { - if (shell != null && location != null) { - shell.setLocation(location); - } - } - - @Override - public void setSizeConstraints(int maxWidth, int maxHeight) { - } - - @Override - public void dispose() { - if (shell != null && !shell.isDisposed()) { - shell.dispose(); - } - } - - @Override - public void setVisible(boolean visible) { - if (shell != null) { - shell.setVisible(visible); - } - } - - @Override - public void setForegroundColor(org.eclipse.swt.graphics.Color color) { - } - - @Override - public void setBackgroundColor(org.eclipse.swt.graphics.Color color) { - } - - @Override - public boolean isFocusControl() { - return shell != null && shell.isFocusControl(); - } - - @Override - public void setFocus() { - if (shell != null) { - shell.setFocus(); - } - } - - @Override - public void addDisposeListener(org.eclipse.swt.events.DisposeListener listener) { - if (shell != null) { - shell.addDisposeListener(listener); - } - } - - @Override - public void removeDisposeListener(org.eclipse.swt.events.DisposeListener listener) { - if (shell != null) { - shell.removeDisposeListener(listener); - } - } - - @Override - public Point computeSizeHint() { - return new Point(400, 200); - } - - @Override - public void addFocusListener(FocusListener listener) { - if (shell != null) { - shell.addFocusListener(listener); - } - } - - @Override - public void removeFocusListener(FocusListener listener) { - if (shell != null) { - shell.removeFocusListener(listener); - } - } - } -} - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java deleted file mode 100644 index 88ff5aa2..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/CxIgnoredProblemsView.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.ignored; - -import java.util.List; - -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.jface.viewers.TreeViewer; -import org.eclipse.swt.SWT; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Menu; -import org.eclipse.swt.widgets.MenuItem; -import org.eclipse.ui.part.ViewPart; - -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.devassist.ui.findings.ignored.IgnoredProblemsStore.IgnoredProblemsListener; - -/** - * Custom view for displaying ignored problems/findings. Shows problems that have been - * explicitly ignored from the native Problems View and findings from the Findings View. - * Allows restoring problems/findings back to the active list. - */ -public class CxIgnoredProblemsView extends ViewPart implements IgnoredProblemsListener { - - public static final String ID = "com.checkmarx.eclipse.devassist.ui.findings.ignored.CxIgnoredProblemsView"; - - private TreeViewer treeViewer; - private IgnoredProblemsStore ignoredStore; - private List allIssues; - - @Override - public void createPartControl(Composite parent) { - System.out.println("[IGNORED-VIEW] Creating Ignored Problems View..."); - - ignoredStore = IgnoredProblemsStore.getInstance(); - ignoredStore.addListener(this); - - // Create TreeViewer - treeViewer = new TreeViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER); - treeViewer.setContentProvider(new IgnoredProblemsContentProvider()); - treeViewer.setLabelProvider(new IgnoredProblemsLabelProvider()); - treeViewer.setInput(new java.util.ArrayList<>()); - - // Setup toolbar - setupToolbar(); - - // Setup context menu - setupContextMenu(); - - // Setup double-click listener - treeViewer.addDoubleClickListener(event -> { - ISelection selection = event.getSelection(); - if (selection instanceof IStructuredSelection) { - Object selected = ((IStructuredSelection) selection).getFirstElement(); - if (selected instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) selected; - navigateToIgnoredIssue(issue); - } - } - }); - - System.out.println("[IGNORED-VIEW] ✓ Ignored Problems View created"); - } - - private void setupToolbar() { - IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); - - Action restoreAllAction = new Action("Restore All Ignored Problems") { - @Override - public void run() { - System.out.println("[IGNORED-VIEW] Restoring all ignored problems..."); - ignoredStore.clearAll(); - refreshView(); - } - }; - restoreAllAction.setToolTipText("Restore all ignored problems to active findings"); - toolbarManager.add(restoreAllAction); - - Action clearAllAction = new Action("Clear Ignored List") { - @Override - public void run() { - System.out.println("[IGNORED-VIEW] Clearing all ignored problems permanently..."); - ignoredStore.clearAll(); - refreshView(); - } - }; - clearAllAction.setToolTipText("Permanently clear the ignored problems list"); - toolbarManager.add(clearAllAction); - } - - private void setupContextMenu() { - Menu contextMenu = new Menu(treeViewer.getTree()); - treeViewer.getTree().setMenu(contextMenu); - - MenuItem restoreItem = new MenuItem(contextMenu, SWT.PUSH); - restoreItem.setText("Restore This Finding"); - restoreItem.addListener(SWT.Selection, event -> { - IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); - if (selection.getFirstElement() instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) selection.getFirstElement(); - System.out.println("[IGNORED-VIEW] Restoring finding: " + issue.getScanIssueId()); - ignoredStore.restoreProblem(issue.getScanIssueId()); - refreshView(); - } - }); - - new MenuItem(contextMenu, SWT.SEPARATOR); - - MenuItem navigateItem = new MenuItem(contextMenu, SWT.PUSH); - navigateItem.setText("Go to Line"); - navigateItem.addListener(SWT.Selection, event -> { - IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); - if (selection.getFirstElement() instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) selection.getFirstElement(); - navigateToIgnoredIssue(issue); - } - }); - - new MenuItem(contextMenu, SWT.SEPARATOR); - - MenuItem deleteItem = new MenuItem(contextMenu, SWT.PUSH); - deleteItem.setText("Delete from Ignore List"); - deleteItem.addListener(SWT.Selection, event -> { - IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection(); - if (selection.getFirstElement() instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) selection.getFirstElement(); - System.out.println("[IGNORED-VIEW] Permanently removing from ignore list: " + issue.getScanIssueId()); - ignoredStore.restoreProblem(issue.getScanIssueId()); - refreshView(); - } - }); - } - - private void navigateToIgnoredIssue(ScanIssue issue) { - if (issue != null && issue.getFilePath() != null) { - int lineNumber = (issue.getLocations() != null && !issue.getLocations().isEmpty()) - ? issue.getLocations().get(0).getLine() : 0; - System.out.println("[IGNORED-VIEW] Navigating to: " + issue.getFilePath() + " line " + lineNumber); - org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { - try { - org.eclipse.core.resources.IWorkspaceRoot root = org.eclipse.core.resources.ResourcesPlugin - .getWorkspace().getRoot(); - // Find file in workspace by simple name - String simpleName = new org.eclipse.core.runtime.Path(issue.getFilePath()).lastSegment(); - final org.eclipse.core.resources.IFile[] found = new org.eclipse.core.resources.IFile[1]; - - root.accept(proxy -> { - if (proxy.getType() == org.eclipse.core.resources.IResource.FILE && - proxy.getName().equals(simpleName)) { - found[0] = (org.eclipse.core.resources.IFile) proxy.requestResource(); - return false; - } - return true; - }, org.eclipse.core.resources.IResource.NONE); - - if (found[0] != null) { - org.eclipse.ui.IWorkbenchWindow window = org.eclipse.ui.PlatformUI.getWorkbench() - .getActiveWorkbenchWindow(); - if (window != null) { - org.eclipse.ui.IWorkbenchPage page = window.getActivePage(); - if (page != null) { - // Position cursor at the issue line using temporary marker - if (lineNumber > 0) { - positionCursorAtLineWithMarker(page, found[0], lineNumber); - } else { - // No line info, just open the file - org.eclipse.ui.ide.IDE.openEditor(page, found[0], true); - System.out.println("[IGNORED-VIEW] ✓ Opened file: " + simpleName); - } - } - } - } else { - System.out.println("[IGNORED-VIEW] File not found in workspace: " + simpleName); - } - } catch (Exception e) { - System.err.println("[IGNORED-VIEW] Error navigating: " + e.getMessage()); - e.printStackTrace(); - } - }); - } - } - - /** - * Position cursor using temporary marker - same approach as native Problems View. - * This is the most reliable method that works with all Eclipse editors. - */ - private void positionCursorAtLineWithMarker(org.eclipse.ui.IWorkbenchPage page, - org.eclipse.core.resources.IFile file, int lineNumber) { - try { - // Create temporary marker with line number - org.eclipse.core.resources.IMarker tempMarker = file.createMarker("org.eclipse.core.resources.textmarker"); - tempMarker.setAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, lineNumber); - tempMarker.setAttribute(org.eclipse.core.resources.IMarker.TRANSIENT, true); - - // Open editor - org.eclipse.ui.IEditorPart editor = org.eclipse.ui.ide.IDE.openEditor(page, file, true); - System.out.println("[IGNORED-VIEW] ✓ Opened file: " + file.getName()); - - // Use IDE.gotoMarker to position cursor (same as native Problems View) - if (editor != null) { - org.eclipse.ui.ide.IDE.gotoMarker(editor, tempMarker); - System.out.println("[IGNORED-VIEW] ✓ Cursor positioned at line " + lineNumber); - } - - // Delete the temporary marker - try { - tempMarker.delete(); - } catch (Exception e) { - System.out.println("[IGNORED-VIEW] Could not delete temporary marker: " + e.getMessage()); - } - } catch (Exception e) { - System.err.println("[IGNORED-VIEW] Error positioning cursor: " + e.getMessage()); - // Fallback: just open the file - try { - org.eclipse.ui.ide.IDE.openEditor(page, file, true); - System.out.println("[IGNORED-VIEW] ✓ Opened file (fallback): " + file.getName()); - } catch (Exception fallbackEx) { - System.err.println("[IGNORED-VIEW] Error in fallback: " + fallbackEx.getMessage()); - } - } - } - - @Override - public void onIgnoredProblemsChanged() { - System.out.println("[IGNORED-VIEW] Ignored problems changed, refreshing view..."); - refreshView(); - } - - private void refreshView() { - if (treeViewer != null && !treeViewer.getTree().isDisposed()) { - try { - // Get all ignored issues including cached findings from the Findings View - List ignoredIssues = ignoredStore.getAllIgnoredProblems(allIssues); - - System.out.println("[IGNORED-VIEW] Displaying " + ignoredIssues.size() + - " ignored findings (Total known: " + (allIssues != null ? allIssues.size() : 0) + ")"); - - treeViewer.setInput(ignoredIssues); - treeViewer.expandAll(); - } catch (Exception e) { - System.err.println("[IGNORED-VIEW] Error refreshing view: " + e.getMessage()); - e.printStackTrace(); - } - } - } - - /** - * Update the view with all issues. - */ - public void updateIssues(List issues) { - this.allIssues = issues; - refreshView(); - } - - @Override - public void setFocus() { - if (treeViewer != null) { - treeViewer.getTree().setFocus(); - } - } - - @Override - public void dispose() { - ignoredStore.removeListener(this); - super.dispose(); - } -} - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java deleted file mode 100644 index f4f2a9c4..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsContentProvider.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.ignored; - -import org.eclipse.jface.viewers.ITreeContentProvider; -import org.eclipse.jface.viewers.Viewer; - -import com.checkmarx.eclipse.devassist.model.ScanIssue; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Content provider for ignored problems tree view. Organizes issues by file. - */ -public class IgnoredProblemsContentProvider implements ITreeContentProvider { - - private Map> fileToIssues = new HashMap<>(); - - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - fileToIssues.clear(); - if (newInput instanceof List) { - @SuppressWarnings("unchecked") - List issues = (List) newInput; - for (ScanIssue issue : issues) { - String fileName = extractFileName(issue.getFilePath()); - fileToIssues.computeIfAbsent(fileName, k -> new ArrayList<>()).add(issue); - } - } - } - - @Override - public Object[] getElements(Object inputElement) { - return fileToIssues.keySet().toArray(); - } - - @Override - public Object[] getChildren(Object parentElement) { - if (parentElement instanceof String) { - List issues = fileToIssues.get(parentElement); - return issues != null ? issues.toArray() : new Object[0]; - } - return new Object[0]; - } - - @Override - public Object getParent(Object element) { - if (element instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) element; - return extractFileName(issue.getFilePath()); - } - return null; - } - - @Override - public boolean hasChildren(Object element) { - if (element instanceof String) { - List issues = fileToIssues.get(element); - return issues != null && !issues.isEmpty(); - } - return false; - } - - private String extractFileName(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return "Unknown"; - } - int lastSeparator = Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')); - return lastSeparator >= 0 ? filePath.substring(lastSeparator + 1) : filePath; - } - - @Override - public void dispose() { - fileToIssues.clear(); - } -} - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java deleted file mode 100644 index 285de9b7..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsLabelProvider.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.checkmarx.eclipse.devassist.ui.findings.ignored; - -import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider; -import org.eclipse.jface.viewers.ILabelProviderListener; -import org.eclipse.jface.viewers.StyledString; -import org.eclipse.jface.viewers.StyledString.Styler; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.TextStyle; - -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.devassist.ui.findings.icons.IconRegistry; - -/** - * Label provider for ignored problems tree view. Renders severity icons and - * formatted text with strikethrough styling to indicate ignored status. - */ -public class IgnoredProblemsLabelProvider extends DelegatingStyledCellLabelProvider { - - public IgnoredProblemsLabelProvider() { - super(new IStyledLabelProvider() { - @Override - public StyledString getStyledText(Object element) { - if (element instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) element; - String lineNum = issue.getLocations() != null && !issue.getLocations().isEmpty() - ? String.valueOf(issue.getLocations().get(0).getLine()) : "?"; - String text = "[" + issue.getSeverity().toUpperCase() + "] " + issue.getTitle() + - " (Line " + lineNum + ")"; - StyledString styledText = new StyledString(text); - // Strikethrough style for ignored problems - Styler strikethrough = new Styler() { - @Override - public void applyStyles(TextStyle textStyle) { - textStyle.strikeout = true; - } - }; - styledText.setStyle(0, text.length(), strikethrough); - return styledText; - } else if (element instanceof String) { - return new StyledString((String) element, StyledString.QUALIFIER_STYLER); - } - return new StyledString(""); - } - - @Override - public Image getImage(Object element) { - if (element instanceof ScanIssue) { - ScanIssue issue = (ScanIssue) element; - if (issue.getSeverity() != null) { - try { - return IconRegistry.getIcon(issue.getSeverity(), IconRegistry.Size.SMALL); - } catch (Exception e) { - return null; - } - } - } - return null; - } - - @Override - public void addListener(ILabelProviderListener listener) {} - - @Override - public void removeListener(ILabelProviderListener listener) {} - - @Override - public void dispose() {} - - @Override - public boolean isLabelProperty(Object element, String property) { - return false; - } - }); - } -} - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java deleted file mode 100644 index 2d6cedf7..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/FindingsEditorHoverListener.java +++ /dev/null @@ -1,92 +0,0 @@ -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.source.ISourceViewer; - -import com.checkmarx.eclipse.devassist.ui.findings.editor.CxFindingsHover; - -/** - * Listens for editor open/close events and installs custom hover handlers for Findings. - * - * This listener handles dynamic installation of hovers on editors that open during - * the session. It installs CxFindingsHover which provides rich vulnerability details - * when hovering over underlined code with FindingsAnnotation. - * - * Works independently from Eclipse's native Problems View. - */ -public class FindingsEditorHoverListener implements IPartListener2 { - - private static final CxFindingsHover findingsHover = new CxFindingsHover(); - - @Override - public void partOpened(IWorkbenchPartReference partRef) { - Object part = partRef.getPart(false); - if (part instanceof IEditorPart) { - installHoverOnEditor((IEditorPart) part); - } - } - - @Override - public void partActivated(IWorkbenchPartReference partRef) { - // Install on activation too, in case it wasn't installed earlier - Object part = partRef.getPart(false); - if (part instanceof IEditorPart) { - installHoverOnEditor((IEditorPart) part); - } - } - - @Override - public void partBroughtToTop(IWorkbenchPartReference partRef) {} - - @Override - public void partClosed(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) {} - - /** - * Install Checkmarx Findings hover handler on the given editor if it has a text viewer. - * - * Installs CxFindingsHover which finds FindingsAnnotations at the hover offset - * and displays detailed vulnerability information. - */ - public void installHoverOnEditor(IEditorPart editor) { - try { - if (editor == null) { - return; - } - - // Adapt editor to ISourceViewer to check if it's a text editor - Object viewer = null; - try { - viewer = editor.getAdapter(ISourceViewer.class); - } catch (Exception e) { - // Not a text editor; skip - return; - } - - if (viewer instanceof ISourceViewer) { - ISourceViewer sourceViewer = (ISourceViewer) viewer; - - // Register findings hover for Java code and JavaDoc content types - sourceViewer.setTextHover(findingsHover, "org.eclipse.jdt.ui.javaCode"); - sourceViewer.setTextHover(findingsHover, "org.eclipse.jdt.ui.javaDocCode"); - - System.out.println("[FINDINGS-HOVER] ✓ Installed hover on editor"); - } - } catch (Exception e) { - System.err.println("[FINDINGS-HOVER] Error installing hover: " + e.getMessage()); - } - } -} 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 49a9e276..2eac1a86 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/startup/PluginStartup.java @@ -8,7 +8,6 @@ import com.checkmarx.eclipse.utils.CxLogger; import com.checkmarx.eclipse.devassist.ui.findings.realtime.CheckmarxEditorListener; -import com.checkmarx.eclipse.devassist.ui.findings.realtime.FindingsEditorHoverListener; import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; import com.checkmarx.eclipse.devassist.backend.listener.ProjectLifecycleListener; @@ -16,7 +15,6 @@ public class PluginStartup implements IStartup { private static final String VIEW_ID = "com.checkmarx.eclipse.views.CheckmarxView"; private static final String FINDINGS_VIEW_ID = "com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView"; - private static FindingsEditorHoverListener hoverListener; // Keep strong reference to prevent GC private static CheckmarxEditorListener realtimeScanListener; // Keep strong reference to prevent GC private static ProjectLifecycleListener projectListener; // Keep strong reference to prevent GC @@ -38,10 +36,6 @@ public void earlyStartup() { page.showView(FINDINGS_VIEW_ID); } - // Register listener for custom hover on findings annotations - hoverListener = new FindingsEditorHoverListener(); - window.getPartService().addPartListener(hoverListener); - // Register listener for real-time scanning with debounce realtimeScanListener = new CheckmarxEditorListener(); window.getPartService().addPartListener(realtimeScanListener); From 3d80f5f0912ee4ad4725f3399fff152cd59feaf8 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Mon, 3 Aug 2026 16:05:38 +0530 Subject: [PATCH 3/9] Scan detection --- .../META-INF/MANIFEST.MF | 2 + .../devassist/backend/ScannerRegistry.java | 115 ++++--- .../listener/ProjectLifecycleListener.java | 2 +- .../basescanner/BaseScannerCommand.java | 111 +++++++ .../basescanner/BaseScannerService.java | 98 +++--- .../devassist/basescanner/ScannerCommand.java | 34 ++ .../devassist/basescanner/ScannerService.java | 68 ++-- .../{basescanner => common}/ScanManager.java | 36 ++- .../devassist/common/ScannerConfig.java | 95 ++++++ .../devassist/common/ScannerFactory.java | 15 +- .../inspection/DevAssistInspectionMgr.java | 2 +- .../listeners/DevAssistProjectListener.java | 303 ++++++++++++++++++ .../scanners/asca/AscaScannerCommand.java | 35 +- .../scanners/asca/AscaScannerService.java | 113 +++---- .../containers/ContainerScannerService.java | 57 ++-- .../scanners/iac/IacScanResultAdaptor.java | 2 +- .../scanners/iac/IacScannerCommand.java | 3 +- .../scanners/iac/IacScannerService.java | 64 ++-- .../scanners/oss/OssScannerCommand.java | 30 +- .../scanners/oss/OssScannerService.java | 123 +++---- .../secrets/SecretsScannerCommand.java | 4 +- .../secrets/SecretsScannerService.java | 68 ++-- .../ui/findings/realtime/RealTimeScanJob.java | 4 +- .../devassist/utils/DevAssistConstants.java | 159 +++++++++ .../devassist/utils/DevAssistUtils.java | 180 +++++++++++ .../eclipse/devassist/utils/ScanEngine.java | 21 ++ 26 files changed, 1306 insertions(+), 438 deletions(-) create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java rename checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/{basescanner => common}/ScanManager.java (81%) create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java create mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java diff --git a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF index 7d2105c1..f8ed4a1d 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -27,6 +27,8 @@ Import-Package: org.eclipse.core.resources, org.osgi.service.event;version="1.4.1" Bundle-ActivationPolicy: lazy Bundle-Activator: com.checkmarx.eclipse.Activator +Export-Package: com.checkmarx.ast.asca, + com.checkmarx.ast.wrapper Bundle-ClassPath: ., lib/slf4j-simple-2.0.17.jar, lib/slf4j-reload4j-2.0.17.jar, diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java index eeb46a5d..c739b7f4 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -164,102 +164,137 @@ private Object createScannerInstance(ScannerType type) { * These are minimal adapters that delegate to the proper scanner packages. */ - private static class OssScannerServiceImpl implements ScannerService { + 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 command.shouldScan(filePath); } + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } @Override - public java.util.List scan(String filePath) throws Exception { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); - return result != null ? result.getIssues() : java.util.List.of(); + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[OSS-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } } @Override - public String getDisplayName() { return "Open Source Supply Chain"; } - @Override - public ScannerType getScannerType() { return ScannerType.OSS; } + 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 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 command.shouldScan(filePath); } + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } @Override - public java.util.List scan(String filePath) throws Exception { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); - return result != null ? result.getIssues() : java.util.List.of(); + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[SECRETS-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } } @Override - public String getDisplayName() { return "Secrets Scanning"; } - @Override - public ScannerType getScannerType() { return ScannerType.SECRETS; } + 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 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 command.shouldScan(filePath); } + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } @Override - public java.util.List scan(String filePath) throws Exception { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); - return result != null ? result.getIssues() : java.util.List.of(); + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[IAC-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } } @Override - public String getDisplayName() { return "Infrastructure as Code"; } - @Override - public ScannerType getScannerType() { return ScannerType.IAC; } + 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 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 command.shouldScan(filePath); } + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } @Override - public java.util.List scan(String filePath) throws Exception { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); - return result != null ? result.getIssues() : java.util.List.of(); + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[ASCA-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } } @Override - public String getDisplayName() { return "Application Security Code Analysis"; } - @Override - public ScannerType getScannerType() { return ScannerType.ASCA; } + 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 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 command.shouldScan(filePath); } + public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } @Override - public java.util.List scan(String filePath) throws Exception { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); - return result != null ? result.getIssues() : java.util.List.of(); + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + try { + var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; + } catch (Exception e) { + CxLogger.error("[CONTAINER-SERVICE] Scan error: " + e.getMessage(), e); + return null; + } } @Override - public String getDisplayName() { return "Container Scanning"; } - @Override - public ScannerType getScannerType() { return ScannerType.CONTAINERS; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } @Override public void close() throws Exception { command.dispose(); } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java index 0184196d..ed233240 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -15,7 +15,7 @@ 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.basescanner.ScanManager; +import com.checkmarx.eclipse.devassist.common.ScanManager; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.runtime.IProgressMonitor; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java new file mode 100644 index 00000000..41f775ba --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java @@ -0,0 +1,111 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.resources.IProject; + +/** + * BaseScannerCommand is an abstract implementation of the ScannerCommand interface + * that provides foundational functionality for registering, deregistering, and + * managing a scanner's lifecycle for a given project. This class serves as a + * base implementation for custom scanner commands. + */ +public abstract class BaseScannerCommand implements ScannerCommand { + + private static final String LOG_TAG = "[SCANNER-COMMAND]"; + public ScannerConfig config; + protected IProject project; + + /** + * 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(); + } + + /** + * 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()); + } + + /** + * 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 project != null && project.isOpen(); + } + + /** + * This method returns the ScanEngine Type + * + * @return ScanEngine + */ + protected ScanEngine getScannerType() { + return ScanEngine.valueOf(config.getEngineName().toUpperCase()); + } + + /** + * Get the configuration. + * + * @return Scanner config + */ + public ScannerConfig getConfig() { + return config; + } + + /** + * Abstract method to initialize the scanner + * This method is invoked when the scanner is registered for the project + */ + @Override + public abstract void initializeScanner(); + + /** + * Dispose the scanner. + */ + @Override + public void dispose() { + CxLogger.info(LOG_TAG + " Disposed"); + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java index c7ba8d8b..a1cd195a 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerService.java @@ -1,9 +1,10 @@ package com.checkmarx.eclipse.devassist.basescanner; import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.IDocument; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -12,36 +13,35 @@ import java.util.stream.Stream; /** - * Base implementation of ScannerService providing common functionality. + * 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. * - * Provides: - * - File filtering (node_modules exclusion, etc.) - * - Temporary folder management - * - Template methods for subclasses + * @param is type of ScanResult produced by concrete scanner Scan method implementations */ -public abstract class BaseScannerService implements ScannerService { +public abstract class BaseScannerService implements ScannerService { protected final IProject project; - protected final String logTag; + public ScannerConfig config; + private static final String LOG_TAG = "[SCANNER-SERVICE]"; /** - * Create a scanner for a project. + * 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) { + public BaseScannerService(IProject project, ScannerConfig config) { this.project = project; - this.logTag = "[" + getScannerName() + "-SCANNER]"; + this.config = config; } /** - * Check if file should be scanned. - * - * Applies common exclusions then delegates to subclass for type checking. + * Determines whether the file at the given path should be scanned. + * Files inside /node_modules/ are skipped by default. * - * @param filePath File path - * @param project Eclipse project - * @return true if file should be scanned + * @param filePath absolute or project-relative file path + * @return true if the file should be scanned; false otherwise */ @Override public boolean shouldScanFile(String filePath) { @@ -50,23 +50,13 @@ public boolean shouldScanFile(String filePath) { } // Common exclusions - if (isCommonlyExcluded(filePath)) { + if (filePath.contains("/node_modules/") || filePath.contains("\\node_modules\\")) { return false; } return isFileTypeSupported(filePath); } - /** - * Apply common exclusions. - * - * @param filePath File path - * @return true if file should be excluded - */ - private boolean isCommonlyExcluded(String filePath) { - return filePath.contains("/node_modules/") || filePath.contains("\\node_modules\\"); - } - /** * Subclasses implement scanner-specific file type checking. * @@ -76,26 +66,29 @@ private boolean isCommonlyExcluded(String filePath) { protected abstract boolean isFileTypeSupported(String filePath); /** - * Get the scanner name for logging (e.g., "OSS", "SECRETS"). + * Perform scan - subclasses must implement this. * - * @return Scanner name + * @param filePath File to scan + * @return ScanResult of type T or null */ - protected abstract String getScannerName(); + @Override + public abstract ScanResult scan(String filePath); /** - * Get the log tag for this scanner. + * Get the configuration. * - * @return Log tag + * @return Scanner config */ - protected String getLogTag() { - return logTag; + @Override + public ScannerConfig getConfig() { + return config; } /** - * Build path to temp sub-folder in system temp directory. + * Builds the path to a temporary sub-folder within the system temp directory. * - * @param baseDir Sub-folder name - * @return Absolute path to 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"); @@ -104,22 +97,22 @@ protected String getTempSubFolderPath(String baseDir) { } /** - * Create temp folder if it doesn't exist. + * Ensures that the specified temporary folder exists, creating any missing directories. * - * @param folderPath Folder path + * @param folderPath target temporary folder path */ protected void createTempFolder(Path folderPath) { try { Files.createDirectories(folderPath); } catch (IOException e) { - CxLogger.warning("Failed to create temp folder: " + folderPath); + CxLogger.warning("Failed to create temporary folder:" + folderPath); } } /** - * Recursively delete temp folder and contents. + * Recursively deletes the provided temporary folder and files in it, if it has been created. * - * @param tempFolder Folder to delete + * @param tempFolder root path of the temporary folder to remove */ protected void deleteTempFolder(Path tempFolder) { if (Files.notExists(tempFolder)) { @@ -127,15 +120,15 @@ protected void deleteTempFolder(Path tempFolder) { } try (Stream walk = Files.walk(tempFolder)) { walk.sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - CxLogger.warning("Failed to delete temp file: " + path); - } - }); + .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 temp folder: " + tempFolder); + CxLogger.warning("Failed to delete temporary folder:" + tempFolder); } } @@ -144,7 +137,8 @@ protected void deleteTempFolder(Path tempFolder) { * * @throws Exception if close fails */ + @Override public void close() throws Exception { - CxLogger.info(logTag + " Closed for project: " + project.getName()); + CxLogger.info(LOG_TAG + " Closed for project: " + project.getName()); } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java new file mode 100644 index 00000000..27057f33 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerCommand.java @@ -0,0 +1,34 @@ +package com.checkmarx.eclipse.devassist.basescanner; + +import org.eclipse.core.resources.IProject; + +/** + * Interface for scanner command implementations. + * Manages scanner lifecycle including registration and deregistration. + */ +public interface ScannerCommand { + + /** + * Register the scanner for a project. + * + * @param project Eclipse project + */ + void register(IProject project); + + /** + * Deregister the scanner for a project. + * + * @param project Eclipse project + */ + void deregister(IProject project); + + /** + * Initialize the scanner. + */ + void initializeScanner(); + + /** + * Dispose the scanner. + */ + void dispose(); +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java index 544df8f0..c1121b7f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScannerService.java @@ -1,73 +1,43 @@ -package com.checkmarx.eclipse.devassist.basescanner; +package com.checkmarx.eclipse.devassist.basescanner; -import java.util.List; - -import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; /** - * Interface for all scanner implementations. + * Generic interface for scanner services. + * Each scanner produces a specific result type T. * - * Each scanner (OSS, Secrets, ASCA, Containers, IAC) implements this - * to provide consistent scan execution and file type detection. + * @param The result type produced by this scanner */ -public interface ScannerService extends AutoCloseable { +public interface ScannerService { /** - * Check if this scanner supports a file type. + * Check if this scanner should scan the file. * - * @param filePath File path to check - * @return true if this scanner can scan this file + * @param filePath File path + * @return true if file should be scanned */ boolean shouldScanFile(String filePath); /** - * Execute a scan on a file. + * Perform a scan on the file and return result. * - * @param filePath Absolute file path to scan - * @return List of issues found by this scanner - * @throws Exception if scan fails + * @param filePath File path + * @return ScanResult of type T or null */ - List scan(String filePath) throws Exception; + ScanResult scan(String filePath); /** - * Get the display name of this scanner. + * Get the scanner configuration. * - * @return Human-readable name (e.g., "Open Source Supply Chain") + * @return Scanner config */ - String getDisplayName(); + ScannerConfig getConfig(); /** - * Get the scanner type. + * Close scanner and release resources. * - * @return Scanner type enum - */ - ScannerType getScannerType(); - - /** - * Cleanup resources when scanner is no longer needed. + * @throws Exception if close fails */ - @Override void close() throws Exception; - - /** - * Scanner type enumeration. - */ - enum ScannerType { - OSS("Open Source Supply Chain"), - SECRETS("Secrets Scanning"), - CONTAINERS("Container Scanning"), - IAC("Infrastructure as Code"), - ASCA("Application Security Code Analysis"); - - private final String displayName; - - ScannerType(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - } } - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java similarity index 81% rename from checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java rename to checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java index 8f06812c..971b7739 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/ScanManager.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.basescanner; +package com.checkmarx.eclipse.devassist.common; import java.util.ArrayList; import java.util.List; @@ -6,7 +6,7 @@ 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.common.ScannerFactory; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.utils.CxLogger; @@ -81,11 +81,12 @@ public List scanFile(String filePath) throws Exception { // 3. Get all scanners that support this file System.out.println(LOG_TAG + " [STEP 3/5] Getting applicable scanners..."); - List applicableScanners = factory.getAllSupportedScanners(filePath); + List> applicableScanners = factory.getAllSupportedScanners(filePath); System.out.println(LOG_TAG + " ✓ Found " + applicableScanners.size() + " applicable scanners:"); - for (ScannerService scanner : applicableScanners) { - System.out.println(LOG_TAG + " - " + scanner.getDisplayName()); + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; + System.out.println(LOG_TAG + " - " + displayName); } if (applicableScanners.isEmpty()) { @@ -100,18 +101,20 @@ public List scanFile(String filePath) throws Exception { List allIssues = new ArrayList<>(); int scannerIndex = 1; - for (ScannerService scanner : applicableScanners) { + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; try { System.out.println(LOG_TAG + " [" + scannerIndex + "/" + applicableScanners.size() + "] Executing " - + scanner.getDisplayName() + "..."); + + displayName + "..."); - List scannerResults = scanner.scan(filePath); + var scanResult = scanner.scan(filePath); + List scannerResults = scanResult != null ? scanResult.getIssues() : null; if (scannerResults == null) { System.out.println( - LOG_TAG + " ⚠️ WARNING: " + scanner.getDisplayName() + " returned NULL results!"); + LOG_TAG + " ⚠️ WARNING: " + displayName + " returned NULL results!"); } else { - System.out.println(LOG_TAG + " ✓ " + scanner.getDisplayName() + " returned " + System.out.println(LOG_TAG + " ✓ " + displayName + " returned " + scannerResults.size() + " issues"); for (ScanIssue issue : scannerResults) { System.out.println( @@ -122,7 +125,7 @@ public List scanFile(String filePath) throws Exception { } catch (Exception e) { // Log but continue with other scanners - System.err.println(LOG_TAG + " ✗ ERROR in " + scanner.getDisplayName() + ": " + e.getMessage()); + System.err.println(LOG_TAG + " ✗ ERROR in " + displayName + ": " + e.getMessage()); e.printStackTrace(); } scannerIndex++; @@ -154,18 +157,19 @@ public List scanFileWithScanner(String filePath, ScannerType scannerT CxLogger.info(LOG_TAG + " Starting " + scannerType.getDisplayName() + " scan: " + filePath); - ScannerService scanner = factory.getScannerForFile(filePath, scannerType); + ScannerService scanner = factory.getScannerForFile(filePath, scannerType); if (scanner == null) { - CxLogger.warning(LOG_TAG + " " + scannerType.getDisplayName() + " does not support file: " + filePath); + CxLogger.warning(LOG_TAG + " Scanner does not support file: " + filePath); return List.of(); } try { - List results = scanner.scan(filePath); - CxLogger.info(LOG_TAG + " ✓ " + scannerType.getDisplayName() + " found " + results.size() + " issues"); + 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 + " " + scannerType.getDisplayName() + " scan failed: " + e.getMessage(), e); + CxLogger.error(LOG_TAG + " Scan failed: " + e.getMessage(), e); throw e; } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java new file mode 100644 index 00000000..11a88458 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerConfig.java @@ -0,0 +1,95 @@ +package com.checkmarx.eclipse.devassist.common; + +/** + * Configuration object for scanner engines. + * Defines settings and messages for each scanner type. + */ +public class ScannerConfig { + + private final String engineName; + private final String configSection; + private final String activateKey; + private final String enabledMessage; + private final String disabledMessage; + private final String errorMessage; + + private ScannerConfig(Builder builder) { + this.engineName = builder.engineName; + this.configSection = builder.configSection; + this.activateKey = builder.activateKey; + this.enabledMessage = builder.enabledMessage; + this.disabledMessage = builder.disabledMessage; + this.errorMessage = builder.errorMessage; + } + + public static Builder builder() { + return new Builder(); + } + + public String getEngineName() { + return engineName; + } + + public String getConfigSection() { + return configSection; + } + + public String getActivateKey() { + return activateKey; + } + + public String getEnabledMessage() { + return enabledMessage; + } + + public String getDisabledMessage() { + return disabledMessage; + } + + public String getErrorMessage() { + return errorMessage; + } + + public static class Builder { + private String engineName; + private String configSection; + private String activateKey; + private String enabledMessage; + private String disabledMessage; + private String errorMessage; + + public Builder engineName(String engineName) { + this.engineName = engineName; + return this; + } + + public Builder configSection(String configSection) { + this.configSection = configSection; + return this; + } + + public Builder activateKey(String activateKey) { + this.activateKey = activateKey; + return this; + } + + public Builder enabledMessage(String enabledMessage) { + this.enabledMessage = enabledMessage; + return this; + } + + public Builder disabledMessage(String disabledMessage) { + this.disabledMessage = disabledMessage; + return this; + } + + public Builder errorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + public ScannerConfig build() { + return new ScannerConfig(this); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java index 3e39f9fd..cd0e3e70 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java @@ -8,7 +8,6 @@ import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; import com.checkmarx.eclipse.devassist.basescanner.ScannerService; import com.checkmarx.eclipse.utils.CxLogger; -import org.eclipse.core.resources.IProject; /** * Factory for selecting appropriate scanners by file type. @@ -48,8 +47,8 @@ public ScannerFactory(ScannerRegistry registry) { * @param filePath File to scan * @return List of applicable scanners (empty if none match) */ - public List getAllSupportedScanners(String filePath) { - List supported = new ArrayList<>(); + public List> getAllSupportedScanners(String filePath) { + List> supported = new ArrayList<>(); CxLogger.info(LOG_TAG + " Finding scanners for: " + filePath); @@ -62,7 +61,7 @@ public List getAllSupportedScanners(String filePath) { } // Get scanner from registry - ScannerService scanner = getScannerService(type); + ScannerService scanner = getScannerService(type); if (scanner == null) { CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); continue; @@ -93,7 +92,7 @@ public List getAllSupportedScanners(String filePath) { * @param type Scanner type to retrieve * @return Scanner if enabled and supports file, null otherwise */ - public ScannerService getScannerForFile(String filePath, ScannerType type) { + public ScannerService getScannerForFile(String filePath, ScannerType type) { if (filePath == null || type == null) { return null; } @@ -105,7 +104,7 @@ public ScannerService getScannerForFile(String filePath, ScannerType type) { } // Get scanner from registry - ScannerService scanner = getScannerService(type); + ScannerService scanner = getScannerService(type); if (scanner == null) { CxLogger.warning(LOG_TAG + " Scanner not initialized: " + type); return null; @@ -128,10 +127,10 @@ public ScannerService getScannerForFile(String filePath, ScannerType type) { * @param type Scanner type * @return Scanner instance, or null if not available */ - private ScannerService getScannerService(ScannerType type) { + private ScannerService getScannerService(ScannerType type) { try { Object scanner = registry.getScannerService(type); - return scanner instanceof ScannerService ? (ScannerService) scanner : null; + return scanner instanceof ScannerService ? (ScannerService) scanner : null; } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error getting scanner for type " + type + ": " + e.getMessage()); return null; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java index 2690357c..d09fdd74 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java @@ -9,8 +9,8 @@ import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.IDocument; -import com.checkmarx.eclipse.devassist.basescanner.ScanManager; 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; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java new file mode 100644 index 00000000..835412a1 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java @@ -0,0 +1,303 @@ +package com.checkmarx.eclipse.devassist.listeners; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.QualifiedName; +import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.result.ResultPublisher; +import com.checkmarx.eclipse.devassist.common.ScanManager; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.utils.CxLogger; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; + +/** + * ProjectListener is responsible for listening for project open/close events and + * managing scanner registration and deregistration for each project. + */ +public class DevAssistProjectListener implements IResourceChangeListener { + + private static final String LOG_TAG = "[PROJECT-LISTENER]"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; + + private static final QualifiedName REGISTRY_KEY = new QualifiedName(PLUGIN_ID, "scanner-registry"); + private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); + private static final QualifiedName STATE_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "state-holder"); + + private final List initializedProjects = new ArrayList<>(); + + /** + * Register this listener with Eclipse workspace and process existing open projects. + */ + public void register() { + CxLogger.info(LOG_TAG + " Registering project lifecycle listener"); + ResourcesPlugin.getWorkspace().addResourceChangeListener( + this, + IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE + ); + CxLogger.info(LOG_TAG + " ✓ Registered"); + + initExistingProjects(); + } + + /** + * Re-runs initialization for any already-open projects. + */ + public void scanAlreadyOpenProjects() { + initExistingProjects(); + } + + /** + * Scans the workspace and initializes any projects that are already open. + */ + private void initExistingProjects() { + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (IProject project : projects) { + if (project.isOpen() && !isInitialized(project)) { + System.out.println(LOG_TAG + " Found existing open project on startup: " + project.getName()); + 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 { + if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + IResource resource = event.getResource(); + if (resource instanceof IProject) { + onProjectClose((IProject) resource); + } + return; + } + + 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; + if ((delta.getFlags() & IResourceDelta.OPEN) != 0) { + if (project.isOpen() && !isInitialized(project)) { + onProjectOpen(project); + } else if (!project.isOpen() && isInitialized(project)) { + onProjectClose(project); + } + } + } + return true; + }); + } + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error handling resource change: " + e.getMessage(), e); + } + } + + private void onProjectOpen(IProject project) { + String projName = project.getName(); + if (projName.length() > 26) projName = projName.substring(0, 26); + try { + if (!isUserAuthenticated()) { + return; + } + + ScannerRegistry registry = new ScannerRegistry(project); + registry.registerAllScanners(); + project.setSessionProperty(REGISTRY_KEY, registry); + + ProblemHolderService problemHolder = new ProblemHolderService(); + project.setSessionProperty(PROBLEM_HOLDER_KEY, problemHolder); + DevAssistScanStateHolder stateHolder = new DevAssistScanStateHolder(); + project.setSessionProperty(STATE_HOLDER_KEY, stateHolder); + initializedProjects.add(project.getName()); + + startWorkspaceFileScanning(project); + + } catch (Exception e) { + e.printStackTrace(); + CxLogger.error(LOG_TAG + " Error initializing project " + + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isUserAuthenticated() { + String apiKey = com.checkmarx.eclipse.properties.Preferences.getApiKey(); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + private void onProjectClose(IProject project) { + CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); + + try { + try { + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); + if (registry != null) { + registry.deregisterAllScanners(); + CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error disposing ScannerRegistry: " + e.getMessage()); + } + + try { + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + if (problemHolder != null) { + problemHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing cache: " + e.getMessage()); + } + + try { + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + if (stateHolder != null) { + stateHolder.clearAll(); + CxLogger.info(LOG_TAG + " ✓ State holder cleared"); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing state: " + e.getMessage()); + } + + initializedProjects.remove(project.getName()); + CxLogger.info(LOG_TAG + " ✓ Project cleanup completed: " + project.getName()); + + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error cleaning up project " + project.getName() + ": " + e.getMessage(), e); + } + } + + private boolean isInitialized(IProject project) { + return initializedProjects.contains(project.getName()); + } + + public String getStatistics() { + return "Initialized projects: " + initializedProjects.size(); + } + + private void startWorkspaceFileScanning(IProject project) { + Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Scanning manifest, IaC, and container files...", 3); + + scanManifestFiles(project); + monitor.worked(1); + + scanIacFiles(project); + monitor.worked(1); + + scanContainerFiles(project); + monitor.worked(1); + + return Status.OK_STATUS; + + } catch (Exception e) { + e.printStackTrace(); + return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); + } finally { + monitor.done(); + } + } + }; + + scanJob.setPriority(Job.BUILD); + scanJob.schedule(); + } + + private void scanManifestFiles(IProject project) { + String[] manifestPatterns = { + "pom.xml", "package.json", "package-lock.json", "npm-shrinkwrap.json", + "go.mod", "go.sum", "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", + "Gemfile", "Gemfile.lock", "Cargo.toml", "Cargo.lock", "composer.json", "composer.lock", + "packages.config", ".csproj", "yarn.lock" + }; + findAndScanFiles(project, manifestPatterns, "OSS Manifest Files"); + } + + private void scanIacFiles(IProject project) { + String[] iacPatterns = { ".tf", ".tfvars", ".yaml", ".yml", ".hcl" }; + findAndScanFiles(project, iacPatterns, "IaC Configuration Files"); + } + + private void scanContainerFiles(IProject project) { + String[] containerPatterns = { + "Dockerfile", "dockerfile", "docker-compose.yaml", "docker-compose.yml", ".dockerignore" + }; + findAndScanFiles(project, containerPatterns, "Container Files"); + } + + private void findAndScanFiles(IProject project, String[] patterns, String fileType) { + try { + System.out.println(LOG_TAG + " ▶ Scanning for " + fileType + "..."); + + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "scanner-registry")); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "state-holder")); + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new QualifiedName(PLUGIN_ID, "problem-holder")); + + if (registry == null || stateHolder == null || problemHolder == null) { + return; + } + + IResource[] members = project.members(true); + for (IResource resource : members) { + if (!(resource instanceof org.eclipse.core.resources.IFile)) { + continue; + } + + IFile file = (org.eclipse.core.resources.IFile) resource; + String fileName = file.getName().toLowerCase(); + String filePath = file.getLocation().toOSString(); + + boolean matches = false; + for (String pattern : patterns) { + if (fileName.equals(pattern.toLowerCase()) || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { + matches = true; + break; + } + } + if (matches) { + try { + ScanManager scanManager = new ScanManager(registry, stateHolder); + List issues = scanManager.scanFile(filePath); + if (!issues.isEmpty()) { + problemHolder.addScanIssues(filePath, issues); + ResultPublisher.publishResults(file, issues); + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error scanning " + fileName + ": " + e.getMessage()); + } + } + } + } catch (Exception e) { + System.err.println(LOG_TAG + " Error finding files for " + fileType + ": " + e.getMessage()); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java index 0bdee66b..dd0076fe 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerCommand.java @@ -1,36 +1,51 @@ package com.checkmarx.eclipse.devassist.scanners.asca; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.IDocument; /** - * Command for coordinating ASCA scanner operations. + * 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 { +public class AscaScannerCommand extends BaseScannerCommand { - private final IProject project; - private final AscaScannerService scannerService; + 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) { - this.project = project; - this.scannerService = new AscaScannerService(project); + super(project, AscaScannerService.createConfig()); + this.ascaScannerService = new AscaScannerService(project); CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); } - public boolean shouldScan(String filePath) { - return scannerService.shouldScanFile(filePath); + @Override + public void initializeScanner() { + CxLogger.info(LOG_TAG + " Initialized for real-time scanning"); } + /** + * Perform an ASCA scan on a file. + * + * @param filePath File path to scan + * @param document Document content + * @return Scan result + */ public ScanResult scan(String filePath, IDocument document) { - return scannerService.scan(filePath, document, project); + return ascaScannerService.scanWithDocument(filePath, document); } + @Override public void dispose() { try { - scannerService.close(); + ascaScannerService.close(); CxLogger.info(LOG_TAG + " Disposed for project: " + project.getName()); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java index 6c0d7963..683ec706 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -1,9 +1,12 @@ package com.checkmarx.eclipse.devassist.scanners.asca; +import com.checkmarx.ast.asca.ScanResult; import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; @@ -14,7 +17,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; /** * ASCA (Application Source Code Analysis) scanner service. @@ -25,40 +27,38 @@ * * Adapted from JetBrains implementation for Eclipse platform. */ -public class AscaScannerService { +public class AscaScannerService extends BaseScannerService { - private final IProject project; private static final String LOG_TAG = "[ASCA-SERVICE]"; private static final String ASCA_DIR = "CxASCA"; private static final Object SCAN_LOCK = new Object(); - // Supported extensions for ASCA scanning (based on VSCode/JetBrains - // implementation) - private static final String[] SUPPORTED_EXTENSIONS = { "java", "py", "js", "jsx", "ts", "tsx", "go", "rb", "cs", - "cpp" }; - public AscaScannerService(IProject project) { - this.project = project; - } - - private String getScannerName() { - return "ASCA"; - } - - private String getLogTag() { - return LOG_TAG; + super(project, createConfig()); } /** - * Check if file has a supported extension for ASCA scanning. + * Create default ASCA scanner configuration. */ - private boolean isFileTypeSupported(String filePath) { + 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 : SUPPORTED_EXTENSIONS) { + for (String ext : DevAssistConstants.ASCA_SUPPORTED_EXTENSIONS) { if (lowerPath.endsWith("." + ext)) { return true; } @@ -66,22 +66,25 @@ private boolean isFileTypeSupported(String filePath) { return false; } - public boolean shouldScanFile(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return false; - } - String normalized = filePath.replace("\\", "/"); - return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + com.checkmarx.eclipse.devassist.common.ScanResult result = scanWithDocument(filePath, new Document()); + return (com.checkmarx.eclipse.devassist.common.ScanResult) (com.checkmarx.eclipse.devassist.common.ScanResult) result; + } + + /** + * Primary scan method - gets file content and executes scan. + */ + public com.checkmarx.eclipse.devassist.common.ScanResult scanWithDocument(String filePath, IDocument document) { + return scanInternal(filePath, document, project); } + @Override public void close() throws Exception { // No resources to close } - /** - * Primary scan method - gets file content and executes scan. - */ - public ScanResult scan(String filePath, IDocument document, IProject proj) { + private com.checkmarx.eclipse.devassist.common.ScanResult scanInternal(String filePath, IDocument document, IProject proj) { if (!shouldScanFile(filePath)) { return null; } @@ -89,7 +92,7 @@ public ScanResult scan(String filePath, IDocument document, IProject pro // Get file content from document or file system String fileContent = getFileContent(filePath, document); if (fileContent == null) { - CxLogger.warning(getLogTag() + " Could not read file content: " + filePath); + CxLogger.warning(LOG_TAG + " Could not read file content: " + filePath); return null; } // Run ASCA scan with proper temp file management @@ -99,7 +102,7 @@ public ScanResult scan(String filePath, IDocument document, IProject pro } return new AscaScanResultAdaptor((com.checkmarx.ast.asca.ScanResult) rawResults, filePath); } catch (Exception e) { - CxLogger.error(getLogTag() + " Scan failed: " + e.getMessage(), e); + CxLogger.error(LOG_TAG + " Scan failed: " + e.getMessage(), e); return null; } } @@ -147,14 +150,14 @@ private String getFileContent(String filePath, IDocument document) { return java.nio.file.Files.readString(nioPath, java.nio.charset.StandardCharsets.UTF_8); } } catch (java.io.IOException e) { - CxLogger.warning(getLogTag() + " Failed to read file content from disk: " + e.getMessage()); + 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(getLogTag() + " File reading interrupted for: " + filePath); + CxLogger.warning(LOG_TAG + " File reading interrupted for: " + filePath); } else { - CxLogger.warning(getLogTag() + " Unexpected error reading file: " + e.getMessage()); + CxLogger.warning(LOG_TAG + " Unexpected error reading file: " + e.getMessage()); } } return null; @@ -168,15 +171,15 @@ private Object runAscaScan(String filePath, String fileContent) { synchronized (SCAN_LOCK) { String tempFilePath = saveTempFile(Paths.get(filePath).getFileName().toString(), fileContent); if (tempFilePath == null) { - CxLogger.warning(getLogTag() + " Failed to create temporary file"); + CxLogger.warning(LOG_TAG + " Failed to create temporary file"); return null; } try { - CxLogger.info(getLogTag() + " Starting ASCA scan: " + filePath); + CxLogger.info(LOG_TAG + " Starting ASCA scan: " + filePath); String ignoreFilePath = getIgnoreFilePath(); Object scanResult = executeAscaScanner(tempFilePath, ignoreFilePath); - CxLogger.info(getLogTag() + " ASCA scan completed"); + CxLogger.info(LOG_TAG + " ASCA scan completed"); return scanResult; } finally { deleteFile(tempFilePath); @@ -191,7 +194,7 @@ private Object executeAscaScanner(String filePath, String ignoreFilePath) { try { return scanAscaFile(filePath, true, "Eclipse", ignoreFilePath); } catch (Exception e) { - CxLogger.error(getLogTag() + " ASCA scan error: " + e.getMessage(), e); + CxLogger.error(LOG_TAG + " ASCA scan error: " + e.getMessage(), e); return null; } } @@ -261,14 +264,6 @@ private String saveTempFile(String fileName, String fileContent) { } } - /** - * Create temp folder if it doesn't exist. - */ - private void createTempFolder(Path tempDir) throws IOException { - if (!Files.exists(tempDir)) { - Files.createDirectories(tempDir); - } - } /** * Sanitize file name to prevent directory traversal attacks. @@ -317,32 +312,22 @@ private void deleteFile(String filePath) { // Security check: only delete files in temp directory if (!path.startsWith(tempDir)) { - CxLogger.warning(getLogTag() + " Security violation: file outside temp: " + filePath); + CxLogger.warning(LOG_TAG + " Security violation: file outside temp: " + filePath); return; } Files.deleteIfExists(path); - CxLogger.info(getLogTag() + " Temporary file deleted: " + path); + CxLogger.info(LOG_TAG + " Temporary file deleted: " + path); } catch (SecurityException e) { - CxLogger.error(getLogTag() + " Security error deleting file: " + e.getMessage(), e); + CxLogger.error(LOG_TAG + " Security error deleting file: " + e.getMessage(), e); } catch (IOException e) { - CxLogger.warning(getLogTag() + " Failed to delete temp file: " + filePath); + CxLogger.warning(LOG_TAG + " Failed to delete temp file: " + filePath); } catch (Exception e) { - CxLogger.warning(getLogTag() + " Unexpected error deleting temp file: " + e.getMessage()); + CxLogger.warning(LOG_TAG + " Unexpected error deleting temp file: " + e.getMessage()); } } - /** - * Compatibility method matching basescanner.ScannerService interface. - */ - public List scan(String filePath) throws Exception { - if (!shouldScanFile(filePath)) { - return List.of(); - } - var result = scan(filePath, new Document(), project); - return result != null ? result.getIssues() : List.of(); - } private com.checkmarx.ast.asca.ScanResult scanAscaFile(String path, boolean ascaLatestVersion, String agent, String ignoreFilePath) throws IOException, CxException, InterruptedException { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index 0c2bc906..2be3e68d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -1,10 +1,14 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.IDocument; @@ -25,7 +29,7 @@ * Handles file detection (Docker, Docker Compose, Helm), secure temporary folder management, * and direct invocation of Checkmarx Container Realtime scanning via CxWrapperFactory. */ -public class ContainerScannerService { +public class ContainerScannerService extends BaseScannerService { private static final String LOG_TAG = "[CONTAINER-SERVICE]"; private static final String CONTAINER_DIR = "CxContainer"; @@ -46,30 +50,28 @@ public class ContainerScannerService { "values.yml" ); - private final IProject project; private String fileType; public ContainerScannerService(IProject project) { - this.project = project; + super(project, createConfig()); } /** - * Determines whether a file path or file context should be scanned by evaluating - * container path patterns and Helm configurations. - * - * @param filePath path to evaluate - * @return {@code true} if eligible for scanning; {@code false} otherwise + * Create default Container scanner configuration. */ - public boolean shouldScanFile(String filePath) { - if (filePath == null || filePath.isBlank()) { - return false; - } - - String normalized = filePath.replace("\\", "/"); - if (normalized.contains("/node_modules/")) { - return false; - } + 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); } @@ -250,13 +252,17 @@ private Path getSecureTempDirectory() { return baseTempDir.resolve(CONTAINER_DIR).normalize(); } - private void createTempFolder(Path tempDir) throws IOException { + protected void createTempFolder(Path tempDir) { if (!Files.exists(tempDir)) { - Files.createDirectories(tempDir); + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } } } - private void deleteTempFolder(Path path) { + protected void deleteTempFolder(Path path) { if (path == null || !Files.exists(path)) { return; } @@ -272,16 +278,17 @@ private void deleteTempFolder(Path path) { } /** - * Compatibility method matching base scanner interfaces. + * Compatibility method matching ScannerService interface. */ - public List scan(String filePath) throws Exception { + @Override + public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { - return List.of(); + return null; } - ScanResult result = scan(filePath, null, this.project); - return result != null ? result.getIssues() : List.of(); + return scan(filePath, null, project); } + @Override public void close() throws Exception { // No persistent connections to close } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java index 9360815f..f8d42b37 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -26,7 +26,7 @@ * * Adapted from JetBrains implementation for Eclipse platform. */ -public class IacScanResultAdaptor implements ScanResult { +public class IacScanResultAdaptor implements ScanResult { private static final String LOG_TAG = "[IAC-ADAPTOR]"; private static final String MULTIPLE_ISSUES_SUFFIX = " IaC misconfigurations"; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java index 4174cea3..8861df0a 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java @@ -1,5 +1,6 @@ package com.checkmarx.eclipse.devassist.scanners.iac; +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; @@ -54,7 +55,7 @@ public boolean shouldScan(String filePath) { * @param document editor document content * @return ScanResult containing issues found, or null */ - public ScanResult scan(String filePath, IDocument document) { + public ScanResult scan(String filePath, IDocument document) { return scannerService.scan(filePath, document, project); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java index 920f98b6..8be0d6e3 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -2,10 +2,14 @@ import com.checkmarx.ast.iacrealtime.IacRealtimeResults; import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.apache.commons.lang3.tuple.Pair; import org.eclipse.core.resources.IProject; @@ -24,12 +28,12 @@ /** * Realtime IaC scanner service for Eclipse. - * + * * Manages temporary folder creation, file hash generation, type extraction - * (Terraform, CloudFormation, Kubernetes, Dockerfile, etc.), execution of + * (Terraform, CloudFormation, Kubernetes, Dockerfile, etc.), execution of * Checkmarx IaC real-time scans, and updating ignored issue tracking data. */ -public class IacScannerService { +public class IacScannerService extends BaseScannerService { private static final String LOG_TAG = "[IAC-SERVICE]"; private static final String IAC_DIR = "CxIaC"; @@ -49,22 +53,32 @@ public class IacScannerService { "tf", "tf.json", "yaml", "yml", "json", "dockerfile" ); - private final IProject project; private String fileType; public IacScannerService(IProject project) { - this.project = project; + super(project, createConfig()); } - public String getScannerName() { - return "IAC"; + /** + * 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). */ - public boolean isFileTypeSupported(String filePath) { + @Override + protected boolean isFileTypeSupported(String filePath) { if (filePath == null || filePath.isBlank()) { return false; } @@ -91,17 +105,7 @@ public boolean isFileTypeSupported(String filePath) { return IAC_FILE_EXTENSIONS.contains(fileType); } - /** - * Determines whether a file should be scanned by evaluating general filters and pattern matching. - */ - public boolean shouldScanFile(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return false; - } - String normalized = filePath.replace("\\", "/"); - return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); - } - + @Override public void close() throws Exception { // No resources to release } @@ -110,7 +114,7 @@ public void close() throws Exception { * Primary scan method. Converts editor/document contents to a temporary isolated file * and executes the real-time IaC scan via CxWrapperFactory. */ - public ScanResult scan(String filePath, IDocument document, IProject proj) { + public ScanResult scan(String filePath, IDocument document, IProject proj) { if (!shouldScanFile(filePath)) { return null; } @@ -169,14 +173,14 @@ public ScanResult scan(String filePath, IDocument document, IProject pro } /** - * Compatibility method matching ScannerService interface returning issue lists. + * Compatibility method matching ScannerService interface. */ - public List scan(String filePath) throws Exception { + @Override + public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { - return List.of(); + return null; } - ScanResult result = scan(filePath, new Document(), project); - return result != null ? result.getIssues() : List.of(); + return scan(filePath, new Document(), project); } /** @@ -245,13 +249,17 @@ private Path getSecureTempDirectory() { return Paths.get(tempOSPath, IAC_DIR).toAbsolutePath().normalize(); } - private void createTempFolder(Path tempDir) throws IOException { + protected void createTempFolder(Path tempDir) { if (!Files.exists(tempDir)) { - Files.createDirectories(tempDir); + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } } } - private void deleteTempFolder(Path tempDir) { + protected void deleteTempFolder(Path tempDir) { if (tempDir == null || !Files.exists(tempDir)) { return; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java index 7afb1148..786eb33d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -4,6 +4,7 @@ import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; @@ -37,33 +38,16 @@ public class OssScannerCommand { private static final String LOG_TAG = "[OSS-COMMAND]"; - // Manifest pattern list mirroring DevAssistConstants.MANIFEST_FILE_PATTERNS - private static final List MANIFEST_FILE_PATTERNS = List.of( - "package.json", "package-lock.json", "npm-shrinkwrap.json", - "pom.xml", - "go.mod", "go.sum", - "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", - "Gemfile", "Gemfile.lock", - "Cargo.toml", "Cargo.lock", - "composer.json", "composer.lock", - "packages.config", "*.csproj", - "yarn.lock", ".npm" - ); - public final OssScannerService ossScannerService; private final IProject project; - public OssScannerCommand(IProject project, OssScannerService ossScannerService) { - this.ossScannerService = ossScannerService; + public OssScannerCommand(IProject project) { + this.ossScannerService = new OssScannerService(project); this.project = project; CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); initializeScanner(); } - public OssScannerCommand(IProject project) { - this(project, new OssScannerService(project)); - } - /** * Initializes the scanner, invoked after creation. * Launches a background Eclipse Job to scan all manifest files in the project workspace. @@ -93,7 +77,7 @@ private void scanAllManifestFilesInFolder(IProgressMonitor monitor) { List matchedFiles = new ArrayList<>(); - List pathMatchers = MANIFEST_FILE_PATTERNS.stream() + List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) .collect(Collectors.toList()); @@ -138,7 +122,7 @@ public boolean visit(IResource resource) throws CoreException { String uri = file.getLocation() != null ? file.getLocation().toOSString() : file.getFullPath().toString(); try { // Perform OSS scan using service - ScanResult ossRealtimeResults = ossScannerService.scan(uri, new Document(), project); + ScanResult ossRealtimeResults = ossScannerService.scanWithDocument(uri, new Document()); if (Objects.isNull(ossRealtimeResults)) { CxLogger.warning(LOG_TAG + " Scan failed for manifest file: " + uri); @@ -166,14 +150,14 @@ public boolean shouldScan(String filePath) { * Execute scan on a file with document content. */ public ScanResult scan(String filePath, IDocument document) { - return ossScannerService.scan(filePath, document, project); + return ossScannerService.scanWithDocument(filePath, document); } /** * Execute scan on a file path directly. */ public ScanResult scan(String filePath) { - return ossScannerService.scan(filePath, new Document(), project); + return ossScannerService.scan(filePath); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java index ab97ec9b..981540a9 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -1,16 +1,14 @@ package com.checkmarx.eclipse.devassist.scanners.oss; import com.checkmarx.ast.ossrealtime.OssRealtimeResults; -import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; -import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; @@ -30,69 +28,38 @@ * * Adapted to mirror JetBrains scanner service features. */ -public class OssScannerService { +public class OssScannerService extends BaseScannerService { private static final String LOG_TAG = "[OSS-SERVICE]"; private static final String OSS_DIR = "CxOSS"; private static final Object SCAN_LOCK = new Object(); - private static final List MANIFEST_FILE_PATTERNS = List.of( - "**/Directory.Packages.props", - "**/packages.config", - "**/pom.xml", - "**/package.json", - "**/requirements.txt", - "**/go.mod", - "**/*.csproj", - "**/build.gradle", - "**/build.gradle.kts", - "**/yarn.lock", - "**/*.sbt", - "**/Gemfile", - "**/bower.json", - "**/requirement-*.txt", - "**/requirements-*.txt", - "**/Setup.py", - "**/Setup.cfg", - "**/pyproject.toml", - "**/poetry.lock", - "**/Package.swift", - "**/Package.resolved", - "**/composer.json", - "**/composer.lock", - "**/*.podspec.json", - "**/*.podspec", - "**/Podfile", - "**/Podfile.lock", - "**/Cartfile.resolved", - "**/Gemfile.lock", - "**/Gemfile", - "**/cpanfile.snapshot", - "**/cpanfile", - "**/pubspec.lock" - - ); - - private final IProject project; - public OssScannerService(IProject project) { - this.project = project; - } - - public String getScannerName() { - return "OSS"; + super(project, createConfig()); } /** - * Checks whether the supplied file path matches any of the manifest glob patterns. + * Create default OSS scanner configuration. */ - public boolean isFileTypeSupported(String filePath) { + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.OSS.name()) + .configSection(DevAssistConstants.OSS_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_OSS_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.OSS_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.OSS_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_OSS_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { if (filePath == null) { return false; } Path path = Paths.get(filePath); - List pathMatchers = MANIFEST_FILE_PATTERNS.stream() + List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) .collect(Collectors.toList()); @@ -104,25 +71,20 @@ public boolean isFileTypeSupported(String filePath) { return false; } - /** - * Determines if a given file should be scanned by the OSS scanner. - */ - public boolean shouldScanFile(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return false; - } - String normalized = filePath.replace("\\", "/"); - return !normalized.contains("/node_modules/") && isFileTypeSupported(filePath); - } - + @Override public void close() throws Exception { // No resources to close } + @Override + public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { + return scanWithDocument(filePath, new Document()); + } + /** * Primary scan method - gets file content, isolates into temp folder with companion files, and executes scan. */ - public ScanResult scan(String filePath, IDocument document, IProject proj) { + public ScanResult scanWithDocument(String filePath, IDocument document) { if (!shouldScanFile(filePath)) { return null; } @@ -133,7 +95,7 @@ public ScanResult scan(String filePath, IDocument document, return null; } - Path tempSubFolder = getTempSubFolderPath(filePath); + Path tempSubFolder = getTempSubFolderPathAsPath(filePath); synchronized (SCAN_LOCK) { try { @@ -148,7 +110,6 @@ public ScanResult scan(String filePath, IDocument document, saveCompanionFile(tempSubFolder, filePath); CxLogger.info(LOG_TAG + " Starting Realtime OSS Scan on File: " + filePath); -// String ignoreFilePath = getIgnoreFilePath(proj); OssRealtimeResults scanResults = CxWrapperFactory.build().ossRealtimeScan(mainTempPath.get(), ""); if (scanResults == null) { @@ -157,9 +118,6 @@ public ScanResult scan(String filePath, IDocument document, OssScanResultAdaptor scanResultAdaptor = new OssScanResultAdaptor(scanResults, filePath); - // Performs secondary scan if needed to keep line numbers updated for ignored packages -// updateIgnoredFileDataOnLatestResult(mainTempPath.get(), proj, filePath); - return scanResultAdaptor; } catch (Exception e) { @@ -172,17 +130,6 @@ public ScanResult scan(String filePath, IDocument document, } } - /** - * Compatibility method matching ScannerService interface returning issue lists. - */ - public List scan(String filePath) throws Exception { - if (!shouldScanFile(filePath)) { - return List.of(); - } - ScanResult result = scan(filePath, new Document(), project); - return result != null ? result.getIssues() : List.of(); - } - /** * Performs full scan without passing ignore file to update line numbers of ignored entries. */ @@ -270,7 +217,7 @@ private String getCompanionFileName(String fileName) { /** * Resolves temporary sub-folder path allocated for the file scan. */ - private Path getTempSubFolderPath(String filePath) { + 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(); @@ -306,13 +253,17 @@ private String generateFileHash(String relativePath) { } } - private void createTempFolder(Path tempDir) throws IOException { - if (!Files.exists(tempDir)) { - Files.createDirectories(tempDir); + 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()); } } - private void deleteTempFolder(Path tempDir) { + protected void deleteTempFolder(Path tempDir) { if (tempDir == null || !Files.exists(tempDir)) { return; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java index 6b2b61f2..bfb0f238 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java @@ -1,5 +1,7 @@ package com.checkmarx.eclipse.devassist.scanners.secrets; +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.IDocument; @@ -23,7 +25,7 @@ public boolean shouldScan(String filePath) { return scannerService.shouldScanFile(filePath); } - public SecretsScanResultAdaptor scan(String filePath, IDocument document) { + public ScanResult scan(String filePath, IDocument document) { return scannerService.scan(filePath, document, project); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java index e64c1336..e2b98dce 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -2,10 +2,14 @@ import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.Document; @@ -23,12 +27,12 @@ /** * 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 + * execution of Checkmarx Secrets real-time scans via CxWrapperFactory, and updating * line numbers for ignored secrets. */ -public class SecretsScannerService { +public class SecretsScannerService extends BaseScannerService { private static final String LOG_TAG = "[SECRETS-SERVICE]"; private static final String SECRETS_DIR = "CxSecrets"; @@ -40,14 +44,22 @@ public class SecretsScannerService { "Gemfile", "Cargo.toml", "composer.json", "package-lock.json", "yarn.lock" ); - private final IProject project; - public SecretsScannerService(IProject project) { - this.project = project; + super(project, createConfig()); } - public String getScannerName() { - return "SECRETS"; + /** + * 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(); } /** @@ -75,20 +87,12 @@ private boolean isExcludedFileForSecretsScanning(String filePath) { normalized.contains("/.checkmarxIgnoredTempList"); } - /** - * Checks if the given file is eligible for Secrets scanning. - */ - public boolean shouldScanFile(String filePath) { - if (filePath == null || filePath.isEmpty()) { - return false; - } - String normalized = filePath.replace("\\", "/"); - if (normalized.contains("/node_modules/")) { - return false; - } + @Override + protected boolean isFileTypeSupported(String filePath) { return !isExcludedFileForSecretsScanning(filePath); } + @Override public void close() throws Exception { // No resources to release } @@ -97,12 +101,12 @@ public void close() throws Exception { * Primary scan method. Converts editor/document contents to an isolated temporary file * and executes the real-time Secrets scan via CxWrapperFactory. */ - public SecretsScanResultAdaptor scan(String filePath, IDocument document, IProject proj) { + public ScanResult scan(String filePath, IDocument document, IProject proj) { if (!shouldScanFile(filePath)) { return null; } - Path tempSubFolder = getTempSubFolderPath(filePath); + Path tempSubFolder = getTempSubFolderPathAsPath(filePath); synchronized (SCAN_LOCK) { try { @@ -152,14 +156,14 @@ public SecretsScanResultAdaptor scan(String filePath, IDocument document, IProje } /** - * Compatibility method matching ScannerService interface returning issue lists. + * Compatibility method matching ScannerService interface. */ - public List scan(String filePath) throws Exception { + @Override + public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { - return List.of(); + return null; } - ScanResult result = scan(filePath, new Document(), project); - return result != null ? result.getIssues() : List.of(); + return scan(filePath, new Document(), project); } /** @@ -189,7 +193,7 @@ private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject p /** * Resolves a unique subfolder path for storing the temporary file. */ - private Path getTempSubFolderPath(String originalFilePath) { + private Path getTempSubFolderPathAsPath(String originalFilePath) { Path baseTempPath = getSecureTempDirectory(); String safeFileName = toSafeTempFileName(originalFilePath); return baseTempPath.resolve(safeFileName); @@ -239,13 +243,17 @@ private Path getSecureTempDirectory() { return Paths.get(tempOSPath, SECRETS_DIR).toAbsolutePath().normalize(); } - private void createTempFolder(Path tempDir) throws IOException { + protected void createTempFolder(Path tempDir) { if (!Files.exists(tempDir)) { - Files.createDirectories(tempDir); + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } } } - private void deleteTempFolder(Path tempDir) { + protected void deleteTempFolder(Path tempDir) { if (tempDir == null || !Files.exists(tempDir)) { return; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java index 99236a34..55ece6e3 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java @@ -160,8 +160,8 @@ protected IStatus run(IProgressMonitor monitor) { // Execute backend scanners System.out.println("[REALTIME] [STEP 3/5] Creating ScanManager..."); - com.checkmarx.eclipse.devassist.basescanner.ScanManager scanManager = - new com.checkmarx.eclipse.devassist.basescanner.ScanManager(registry, stateHolder); + com.checkmarx.eclipse.devassist.common.ScanManager scanManager = + new com.checkmarx.eclipse.devassist.common.ScanManager(registry, stateHolder); String filePath = file.getLocation().toOSString(); System.out.println("[REALTIME] [STEP 4/5] Executing backend scanners for: " + filePath); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java new file mode 100644 index 00000000..30ca3d16 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java @@ -0,0 +1,159 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.util.List; + +/** + * The DevAssistConstants class defines a collection of constant values + * related to real-time scanning functionalities, including support for + * different scanning engines and associated configurations. + */ +public final class DevAssistConstants { + + private DevAssistConstants() { + throw new UnsupportedOperationException("Cannot instantiate DevAssistConstants class"); + } + + // Tab Name Constants + public static final String DEVASSIST_TAB = "Checkmarx One Assist Findings"; + public static final String IGNORED_FINDINGS_TAB = "Ignored Findings"; + public static final String DEVASSIST_PLUGIN_FINDINGS_WINDOW_NAME = "Checkmarx Developer Assist Findings"; + + // OSS Scanner Constants + public static final String ACTIVATE_OSS_REALTIME_SCANNER = "Activate OSS-Realtime"; + public static final String OSS_REALTIME_SCANNER = "Checkmarx Open Source Realtime Scanner (OSS-Realtime)"; + public static final String OSS_REALTIME_SCANNER_START = "Realtime OSS Scanner Engine started"; + public static final String OSS_REALTIME_SCANNER_DISABLED = "Realtime OSS Scanner Engine disabled"; + public static final String OSS_REALTIME_SCANNER_DIRECTORY = "Cx-oss-realtime-scanner"; + public static final String ERROR_OSS_REALTIME_SCANNER = "Failed to handle OSS Realtime scan"; + + // Container Scanner Constants + public static final String ACTIVATE_CONTAINER_REALTIME_SCANNER = "Activate Containers-Realtime"; + public static final String CONTAINER_REALTIME_SCANNER = "Checkmarx Containers Realtime Scanner (Containers-Realtime)"; + public static final String CONTAINER_REALTIME_SCANNER_START = "Realtime Containers Scanner Engine started"; + public static final String CONTAINER_REALTIME_SCANNER_DISABLED = "Realtime Containers Scanner Engine disabled"; + public static final String CONTAINER_REALTIME_SCANNER_DIRECTORY = "Cx-containers-realtime-scanner"; + public static final String ERROR_CONTAINER_REALTIME_SCANNER = "Failed to handle Containers Realtime scan"; + + // Secrets Scanner Constants + public static final String ACTIVATE_SECRETS_REALTIME_SCANNER = "Activate Secrets-Realtime"; + public static final String SECRETS_REALTIME_SCANNER = "Checkmarx Secrets Realtime Scanner (Secrets-Realtime)"; + public static final String SECRETS_REALTIME_SCANNER_START = "Realtime Secrets Scanner Engine started"; + public static final String SECRETS_REALTIME_SCANNER_DISABLED = "Realtime Secrets Scanner Engine disabled"; + public static final String SECRETS_REALTIME_SCANNER_DIRECTORY = "Cx-secrets-realtime-scanner"; + public static final String ERROR_SECRETS_REALTIME_SCANNER = "Failed to handle Secrets Realtime scan"; + + // IaC Scanner Constants + public static final String ACTIVATE_IAC_REALTIME_SCANNER = "Activate IAC-Realtime"; + public static final String IAC_REALTIME_SCANNER = "Checkmarx IAC Realtime Scanner (IAC-Realtime)"; + public static final String IAC_REALTIME_SCANNER_START = "Realtime IAC Scanner Engine started"; + public static final String IAC_REALTIME_SCANNER_DISABLED = "Realtime IAC Scanner Engine disabled"; + public static final String IAC_REALTIME_SCANNER_DIRECTORY = "Cx-iac-realtime-scanner"; + public static final String ERROR_IAC_REALTIME_SCANNER = "Failed to handle IAC Realtime scan"; + public static final String IAC_PREREQUISITE = "Please refer IAC RealTime Scanner Prerequisites"; + public static final String IAC_ENGINE_VALIDATION_ERROR = "Checkmarx Containers Management Tool Error"; + + // ASCA Scanner Constants + public static final String ACTIVATE_ASCA_REALTIME_SCANNER = "Activate ASCA-Realtime"; + public static final String ASCA_REALTIME_SCANNER = "Checkmarx AI Secure Coding Assistant (ASCA)"; + public static final String ASCA_REALTIME_SCANNER_START = "AI Secure Coding Assistant Engine started."; + public static final String ASCA_REALTIME_SCANNER_DISABLED = "AI Secure Coding Assistant Engine disabled."; + public static final String ERROR_ASCA_REALTIME_SCANNER = "Failed to handle ASCA Realtime scan"; + + // ASCA Supported File Extensions + public static final List ASCA_SUPPORTED_EXTENSIONS = List.of( + "java", "cs", "go", "py", "js", "jsx", "ts", "tsx", "rb", "cpp" + ); + + // Dev Assist Fixes Constants + public static final String FIX_WITH_CXONE_ASSIST = "Fix with Checkmarx One Assist"; + public static final String FIX_WITH_DEV_ASSIST = "Fix with Checkmarx Developer Assist"; + public static final String VIEW_DETAILS_FIX_NAME = "View details"; + public static final String IGNORE_THIS_VULNERABILITY_FIX_NAME = "Ignore this vulnerability"; + public static final String IGNORE_ALL_OF_THIS_TYPE_FIX_NAME = "Ignore all of this type"; + + // Manifest file patterns + public static final List MANIFEST_FILE_PATTERNS = List.of( + "**/Directory.Packages.props", + "**/packages.config", + "**/pom.xml", + "**/package.json", + "**/requirements.txt", + "**/go.mod", + "**/*.csproj", + "**/build.gradle", + "**/build.gradle.kts", + "**/yarn.lock", + "**/*.sbt", + "**/Gemfile", + "**/bower.json", + "**/requirement-*.txt", + "**/requirements-*.txt", + "**/Setup.py", + "**/Setup.cfg", + "**/pyproject.toml", + "**/poetry.lock", + "**/Package.swift", + "**/Package.resolved", + "**/composer.json", + "**/composer.lock", + "**/*.podspec.json", + "**/*.podspec", + "**/Podfile", + "**/Podfile.lock", + "**/Cartfile.resolved", + "**/Gemfile.lock", + "**/cpanfile.snapshot", + "**/cpanfile", + "**/pubspec.lock" + ); + + // Container file patterns + public static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile", + "**/dockerfile-*", + "**/dockerfile.*", + "**/docker-compose.yml", + "**/docker-compose.yaml", + "**/docker-compose-*.yml", + "**/docker-compose-*.yaml" + ); + + // IaC file patterns and extensions + public static final List IAC_SUPPORTED_PATTERNS = List.of( + "**/dockerfile", + "**/*.auto.tfvars", + "**/*.terraform.tfvars" + ); + + public static final List IAC_FILE_EXTENSIONS = List.of( + "tf", "yaml", "yml", "json", "proto", "dockerfile" + ); + + // Multiple issues on same line + public static final String MULTIPLE_IAC_ISSUES = " IAC issues detected on this line"; + public static final String MULTIPLE_ASCA_ISSUES = " ASCA violations detected on this line"; + + // Container file types + public static final String DOCKERFILE = "dockerfile"; + public static final String DOCKER_COMPOSE = "docker-compose"; + public static final String HELM = "helm"; + public static final List CONTAINER_HELM_EXTENSION = List.of("yml", "yaml"); + public static final List CONTAINER_HELM_EXCLUDED_FILES = List.of("chart.yml", "chart.yaml"); + + // Container image risk descriptions + public static final String MALICIOUS_RISK_CONTAINER = "Malicious-risk container image"; + public static final String CRITICAL_RISK_CONTAINER = "Critical-risk container image"; + public static final String HIGH_RISK_CONTAINER = "High-risk container image"; + public static final String MEDIUM_RISK_CONTAINER = "Medium-risk container image"; + public static final String LOW_RISK_CONTAINER = "Low-risk container image"; + + // General constants + public static final String SEVERITY_PACKAGE = "Severity Package"; + public static final String THEME = "THEME"; + public static final String CX_AGENT_NAME = "Checkmarx One Assist"; + public static final String CX_DEVASSIST_AGENT_NAME = "Checkmarx Developer Assist"; + public static final List AI_AGENT_FILES = List.of("/Dummy.txt", "/", "/AIAssistantInput"); + public static final String SEPARATOR = ":"; + public static final String QUICK_FIX = "QUICK_FIX"; + public static final String UNDO = "Undo"; +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java new file mode 100644 index 00000000..66a0b7c6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -0,0 +1,180 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; + +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.utils.CxLogger; + +/** + * Utility class for DevAssist operations. Provides methods for encoding, decoding, + * severity normalization, and file type detection. + */ +public class DevAssistUtils { + private static final String LOG_TAG = "[DEV-ASSIST-UTILS]"; + + 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; + } +} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java new file mode 100644 index 00000000..26086213 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/ScanEngine.java @@ -0,0 +1,21 @@ +package com.checkmarx.eclipse.devassist.utils; + +/** + * Enumeration representing various scanning engines supported by the system. + * Each constant signifies a specific type of scanning capability provided by the platform. + * + * The available scanning engines are: + * - OSS: Represents scanning for Open Source Software dependencies and vulnerabilities. + * - SECRETS: Represents scanning for sensitive information such as secrets and credentials in the code. + * - CONTAINERS: Represents scanning for vulnerabilities in container images. + * - IAC: Represents scanning for Infrastructure as Code issues and misconfigurations. + * - ASCA: Represents scanning for Application Security Code Analysis. + */ +public enum ScanEngine { + OSS, + SECRETS, + CONTAINERS, + IAC, + ASCA, + ALL +} From c56100426ebfc06e36d104e42b570eb7325f1669 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Mon, 3 Aug 2026 16:20:35 +0530 Subject: [PATCH 4/9] Uneccessary logs removed --- .../listener/ProjectLifecycleListener.java | 18 +- .../backend/result/ResultPublisher.java | 15 +- .../eclipse/devassist/common/ScanManager.java | 45 ++- .../listeners/DevAssistProjectListener.java | 4 +- .../devassist/problems/ProblemDecorator.java | 43 ++- .../problems/ProblemHolderService.java | 8 +- .../devassist/ui/findings/CxFindingsView.java | 270 +++++++++--------- .../actions/VulnerabilityFilterState.java | 2 +- .../editor/FindingsEditorOverlay.java | 22 +- .../findings/icons/SeverityImageComposer.java | 6 +- .../ignored/IgnoredProblemsStore.java | 18 +- .../integration/CopilotIntegration.java | 8 +- .../ui/findings/marker/MarkerIssueMapper.java | 8 +- .../provider/FindingsContentProvider.java | 25 +- .../realtime/CheckmarxEditorListener.java | 22 +- .../ui/findings/realtime/RealTimeScanJob.java | 49 ++-- .../ViewFindingDetailsResolution.java | 36 +-- .../checkmarx/eclipse/utils/PluginUtils.java | 2 +- 18 files changed, 287 insertions(+), 314 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java index ed233240..58c1881d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.backend.listener; +package com.checkmarx.eclipse.devassist.backend.listener; import java.util.ArrayList; import java.util.List; @@ -43,7 +43,7 @@ public void register() { this, IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE ); - CxLogger.info(LOG_TAG + " ✓ Registered"); + CxLogger.info(LOG_TAG + " ✓ Registered"); // FIX 1: Run immediate initialization for projects ALREADY open on IDE startup initExistingProjects(); @@ -71,7 +71,7 @@ private void initExistingProjects() { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (IProject project : projects) { if (project.isOpen() && !isInitialized(project)) { - System.out.println(LOG_TAG + " Found existing open project on startup: " + project.getName()); + onProjectOpen(project); } } @@ -157,14 +157,14 @@ private boolean isUserAuthenticated() { } private void onProjectClose(IProject project) { - CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); + CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); try { try { ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); if (registry != null) { registry.deregisterAllScanners(); - CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); + CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing ScannerRegistry: " + e.getMessage()); @@ -174,7 +174,7 @@ private void onProjectClose(IProject project) { ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); if (problemHolder != null) { problemHolder.clearAll(); - CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); + CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing cache: " + e.getMessage()); @@ -184,14 +184,14 @@ private void onProjectClose(IProject project) { DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); if (stateHolder != null) { stateHolder.clearAll(); - CxLogger.info(LOG_TAG + " ✓ State holder cleared"); + CxLogger.info(LOG_TAG + " ✓ State holder cleared"); } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing state: " + e.getMessage()); } initializedProjects.remove(project.getName()); - CxLogger.info(LOG_TAG + " ✓ Project cleanup completed: " + project.getName()); + 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); @@ -262,7 +262,7 @@ private void scanContainerFiles(IProject project) { private void findAndScanFiles(IProject project, String[] patterns, String fileType) { try { - System.out.println(LOG_TAG + " â–º Scanning for " + fileType + "..."); + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( new QualifiedName(PLUGIN_ID, "scanner-registry")); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java index 797351f2..17257772 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -50,14 +50,14 @@ public static void publishResults(IFile file, List scanIssues) { } try { // Step 1: Update Findings View (try to display immediately if view is open) - System.out.println(LOG_TAG + " [STEP 1/3] Attempting to update Findings View if open..."); + updateFindingsView(file, scanIssues); - System.out.println(LOG_TAG + " [OK] Findings View update attempted"); + // Step 2: Create problem descriptors via DevAssistInspectionMgr - System.out.println(LOG_TAG + " [STEP 2/3] Creating problem descriptors..."); + createAndRenderDecorations(file, scanIssues); - System.out.println(LOG_TAG + " [OK] Problem descriptors created and rendered"); + } catch (Exception e) { System.err.println(LOG_TAG + " [ERROR] " + e.getMessage()); @@ -112,15 +112,14 @@ private static void updateFindingsView(IFile file, List scanIssues) { // Step 1: Remove old results from THIS scanner engine if (engineType != null) { problemHolder.removeScanIssuesByFileAndScanner(engineType, filePath); - System.out.println(LOG_TAG + " [REMOVE] Removed old " + engineType + " issues for: " + filePath); + } // Step 2: Add new results from THIS scanner engine problemHolder.mergeScanIssues(filePath, scanIssues); - System.out.println(LOG_TAG + " [MERGE] Merged " + scanIssues.size() + " new issues from " + - (engineType != null ? engineType : "UNKNOWN") + " for: " + filePath); + } else { - System.out.println(LOG_TAG + " [VIEW-UPDATE] ProblemHolderService not initialized - results not cached"); + } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java index 971b7739..15cd963d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.common; +package com.checkmarx.eclipse.devassist.common; import java.util.ArrayList; import java.util.List; @@ -57,75 +57,70 @@ public ScanManager(ScannerRegistry registry, DevAssistScanStateHolder stateHolde */ public List scanFile(String filePath) throws Exception { if (filePath == null || filePath.isEmpty()) { - System.out.println(LOG_TAG + " ✗ BLOCKED: Null or empty file path"); + return List.of(); } - System.out.println(LOG_TAG + " ╔════════════════════════════════════════════╗"); - System.out.println(LOG_TAG + " â•‘ SCAN MANAGER: Starting file scan â•‘"); - System.out.println(LOG_TAG + " ╚════════════════════════════════════════════╝"); - System.out.println(LOG_TAG + " File path: " + filePath); + + + + // 1. Compute current file state hash - System.out.println(LOG_TAG + " [STEP 1/5] Computing file state hash..."); + long currentStateHash = DevAssistScanStateHolder.computeFileStateHash(filePath); - System.out.println(LOG_TAG + " ✓ File hash: " + currentStateHash); + // 2. Check if file changed since last scan - System.out.println(LOG_TAG + " [STEP 2/5] Checking if file changed..."); + if (!stateHolder.hasChanged(filePath, currentStateHash)) { - System.out.println(LOG_TAG + " ℹ️ File unchanged since last scan - skipping (cache result)"); + return List.of(); } - System.out.println(LOG_TAG + " ✓ File changed - proceeding with scan"); + // 3. Get all scanners that support this file - System.out.println(LOG_TAG + " [STEP 3/5] Getting applicable scanners..."); + List> applicableScanners = factory.getAllSupportedScanners(filePath); - System.out.println(LOG_TAG + " ✓ Found " + applicableScanners.size() + " applicable scanners:"); + for (ScannerService scanner : applicableScanners) { String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; - System.out.println(LOG_TAG + " - " + displayName); + } if (applicableScanners.isEmpty()) { - System.out.println(LOG_TAG + " ℹ️ No scanners support this file type - skipping"); + // Still update state to avoid re-checking unsupported files stateHolder.updateStateHash(filePath, currentStateHash); return List.of(); } // 4. Execute all scanners and merge results - System.out.println(LOG_TAG + " [STEP 4/5] Executing scanners..."); + List allIssues = new ArrayList<>(); int scannerIndex = 1; for (ScannerService scanner : applicableScanners) { String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; try { - System.out.println(LOG_TAG + " [" + scannerIndex + "/" + applicableScanners.size() + "] Executing " - + displayName + "..."); + var scanResult = scanner.scan(filePath); List scannerResults = scanResult != null ? scanResult.getIssues() : null; if (scannerResults == null) { - System.out.println( - LOG_TAG + " ⚠️ WARNING: " + displayName + " returned NULL results!"); + } else { - System.out.println(LOG_TAG + " ✓ " + displayName + " returned " - + scannerResults.size() + " issues"); + for (ScanIssue issue : scannerResults) { - System.out.println( - LOG_TAG + " - " + issue.getTitle() + " (severity: " + issue.getSeverity() + ")"); } allIssues.addAll(scannerResults); } } catch (Exception e) { // Log but continue with other scanners - System.err.println(LOG_TAG + " ✗ ERROR in " + displayName + ": " + e.getMessage()); + System.err.println(LOG_TAG + " ✗ ERROR in " + displayName + ": " + e.getMessage()); e.printStackTrace(); } scannerIndex++; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java index 835412a1..967b3e32 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java @@ -67,7 +67,7 @@ private void initExistingProjects() { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (IProject project : projects) { if (project.isOpen() && !isInitialized(project)) { - System.out.println(LOG_TAG + " Found existing open project on startup: " + project.getName()); + onProjectOpen(project); } } @@ -253,7 +253,7 @@ private void scanContainerFiles(IProject project) { private void findAndScanFiles(IProject project, String[] patterns, String fileType) { try { - System.out.println(LOG_TAG + " ▶ Scanning for " + fileType + "..."); + ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( new QualifiedName(PLUGIN_ID, "scanner-registry")); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java index fdc58e84..293c3693 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.problems; +package com.checkmarx.eclipse.devassist.problems; import java.util.HashMap; import java.util.List; @@ -49,9 +49,6 @@ public class ProblemDecorator { * @param scanIssues Issues to visualize */ public static void decorateEditor(IFile file, List scanIssues) { - System.out.println("[SCAN-DECORATOR-ENTRY] decorateEditor called with " + - (scanIssues != null ? scanIssues.size() : "null") + " issues"); - if (file == null) { return; } @@ -65,33 +62,33 @@ public static void decorateEditor(IFile file, List scanIssues) { // **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(); - System.out.println("[SCAN-DECORATOR-ENTRY] File path: " + filePath); + try { // Find open editor for this file - System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 1/3] Finding open editor..."); + ITextEditor editor = findOpenEditor(file); if (editor == null) { - System.out.println("[SCAN-DECORATOR-ENTRY] ✗ [STEP 1/3] No open editor for: " + filePath); - CxLogger.info(LOG_TAG + " ✗ No open editor for: " + filePath); + + CxLogger.info(LOG_TAG + " ✗ No open editor for: " + filePath); return; } - System.out.println("[SCAN-DECORATOR-ENTRY] ✓ [STEP 1/3] Found editor: " + editor.getClass().getSimpleName()); + // Get annotation model from editor - System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 2/3] Getting annotation model..."); + IAnnotationModel annotationModel = editor.getDocumentProvider() .getAnnotationModel(editor.getEditorInput()); if (annotationModel == null) { - System.out.println("[SCAN-DECORATOR-ENTRY] ✗ [STEP 2/3] Annotation model is NULL"); - CxLogger.warning(LOG_TAG + " ✗ No annotation model available"); + + CxLogger.warning(LOG_TAG + " ✗ No annotation model available"); return; } - System.out.println("[SCAN-DECORATOR-ENTRY] ✓ [STEP 2/3] Got annotation model"); + // Remove previous annotations for this file - System.out.println("[SCAN-DECORATOR-ENTRY] [STEP 3/3] Processing " + scanIssues.size() + " issues..."); + clearAnnotations(filePath, annotationModel); // Add new annotations for each issue @@ -104,7 +101,7 @@ public static void decorateEditor(IFile file, List scanIssues) { annotation.addButton(filePath, null); annotations.add(annotation); - CxLogger.info(LOG_TAG + " ─────────────────────────────────────────────────"); + CxLogger.info(LOG_TAG + " ─────────────────────────────────────────────────"); CxLogger.info(LOG_TAG + " Issue: " + issue.getTitle()); CxLogger.info(LOG_TAG + " Engine: " + issue.getScanEngine()); CxLogger.info(LOG_TAG + " Severity: " + issue.getSeverity()); @@ -130,9 +127,9 @@ public static void decorateEditor(IFile file, List scanIssues) { // Add annotation to model for display annotationModel.addAnnotation(annotation, pos); - CxLogger.info(LOG_TAG + " ✓ Annotation added to model"); + CxLogger.info(LOG_TAG + " ✓ Annotation added to model"); } else { - CxLogger.warning(LOG_TAG + " ✗ FAILED: Invalid position (offset=" + + CxLogger.warning(LOG_TAG + " ✗ FAILED: Invalid position (offset=" + (pos != null ? pos.getOffset() : "null") + ", length=" + (pos != null ? pos.getLength() : "null") + ")"); } @@ -147,10 +144,10 @@ public static void decorateEditor(IFile file, List scanIssues) { // Store annotations for later cleanup fileAnnotations.put(filePath, annotations); - CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); - CxLogger.info(LOG_TAG + " ✓ COMPLETE: Added " + annotations.size() + + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + CxLogger.info(LOG_TAG + " ✓ COMPLETE: Added " + annotations.size() + " annotations to editor"); - CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); + CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error decorating editor: " + @@ -315,7 +312,7 @@ private static org.eclipse.jface.text.Position decorateOssFirstLineOnly( return null; } - CxLogger.info(LOG_TAG + " [OSS] ✓ Decorating first line: [" + lineOffset + + CxLogger.info(LOG_TAG + " [OSS] ✓ Decorating first line: [" + lineOffset + "-" + (lineOffset + decorationLength) + "] = " + decorationLength + " chars"); return new org.eclipse.jface.text.Position(lineOffset, decorationLength); @@ -446,7 +443,7 @@ private static void clearAnnotations(String filePath, } fileAnnotations.remove(filePath); - CxLogger.info(LOG_TAG + " ✓ Cleared " + previousAnnotations.size() + + CxLogger.info(LOG_TAG + " ✓ Cleared " + previousAnnotations.size() + " previous annotations"); } } catch (Exception e) { @@ -541,7 +538,7 @@ public static void clearDecorations(IFile file) { clearAnnotations(filePath, annotationModel); } - CxLogger.info(LOG_TAG + " ✓ Decorations cleared"); + CxLogger.info(LOG_TAG + " ✓ Decorations cleared"); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing decorations: " + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java index 42a2ff62..c552ab6f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.problems; +package com.checkmarx.eclipse.devassist.problems; import java.util.ArrayList; import java.util.Collections; @@ -80,7 +80,7 @@ public List getScanIssuesByFile(String filePath) { /** * Get all cached issues across all files. * - * @return Map of file path → issues + * @return Map of file path → issues */ public Map> getAllScanIssues() { @@ -202,10 +202,10 @@ private void publishIssuesUpdated() { IEventBroker eventBroker = PluginUtils.getEventBroker(); if (eventBroker != null) { Map> allIssues = getAllScanIssues(); - System.out.println(LOG_TAG + " [EVENT-BROKER] Publishing issues update: " + allIssues.size() + " files"); + eventBroker.post(ISSUES_UPDATED_TOPIC, allIssues); } else { - System.err.println(LOG_TAG + " [EVENT-BROKER] ✗ EventBroker not available"); + 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()); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java index f809bd39..f0aefd80 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings; +package com.checkmarx.eclipse.devassist.ui.findings; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; @@ -329,7 +329,7 @@ private void subscribeToEventBroker() { @Override public void dispose() { - System.out.println("[FINDINGS] Disposing CxFindingsView..."); + // 1. Unsubscribe from IEventBroker to prevent memory leaks if (eventHandler != null) { @@ -340,7 +340,7 @@ public void dispose() { if (eventBroker != null) { eventBroker.unsubscribe(eventHandler); - System.out.println("[FINDINGS] ✓ Unsubscribed from IEventBroker"); + } } catch (Exception e) { System.err.println("[FINDINGS] Error unsubscribing from IEventBroker: " + e.getMessage()); @@ -361,7 +361,7 @@ public void dispose() { private void initFindingsViewUI() { try { - System.out.println("[FINDINGS] [INIT-STEP 1/5] Getting workspace projects..."); + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); if (projects.length > 0 && projects[0].isOpen()) { @@ -401,7 +401,7 @@ private void clearToolbar() { } private void setupToolbar() { - System.out.println("[FINDINGS] Setting up toolbar with severity filters..."); + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); // The same IToolBarManager instance survives every re-render of the view, @@ -410,7 +410,7 @@ private void setupToolbar() { // Add filter actions VulnerabilityFilterAction.IFilterChangeListener filterListener = () -> { - System.out.println("[FINDINGS] Filter changed - refreshing tree"); + refreshTreeWithFilter(); }; @@ -468,7 +468,7 @@ public void run() { // Toolbar preferences button Action toolbarPreferencesAction = - new Action("\u2000⋮", Action.AS_PUSH_BUTTON) { + new Action("\u2000?", Action.AS_PUSH_BUTTON) { @Override public void run() { openPreferencesPageAction.run(); @@ -480,17 +480,17 @@ public void run() { toolbar.update(true); getViewSite().getActionBars().updateActionBars(); - System.out.println("[FINDINGS] Toolbar configured with 5 severity filters and preferences button"); + } private void setupTreeListeners() { Tree tree = treeViewer.getTree(); - System.out.println("[FINDINGS] Setting up tree listeners..."); + //Listner for redirection tree.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - System.out.println("[FINDINGS] Single-click detected"); + navigateToSelectedIssue(treeViewer.getSelection()); } }); @@ -500,17 +500,17 @@ public void widgetSelected(SelectionEvent e) { @Override public void mouseDown(MouseEvent e) { if (e.button == 3) { - System.out.println("[FINDINGS] Right-click detected at coordinates: " + e.x + ", " + e.y); + showContextMenu(e); } } }); - System.out.println("[FINDINGS] Tree listeners configured"); + } private void navigateToSelectedIssue(ISelection selection) { - System.out.println("[FINDINGS] Navigating to selected issue..."); + if (selection instanceof IStructuredSelection) { IStructuredSelection ssel = (IStructuredSelection) selection; Object element = ssel.getFirstElement(); @@ -533,10 +533,10 @@ private void navigateToIssue(ScanDetailWithPath detailWithPath) { if (detail.getLocations() != null && !detail.getLocations().isEmpty()) { Location location = detail.getLocations().get(0); - System.out.println("[FINDINGS] Navigating to: " + filePath + ", Line: " + location.getLine()); + openFileInEditor(filePath, location.getLine(), detail); } else { - System.out.println("[FINDINGS] No location information available for issue: " + detail.getTitle()); + } } @@ -567,7 +567,7 @@ private void showIssueDetails(ScanIssue issue) { } details.append("====================================\n"); - System.out.println(details.toString()); + } /** @@ -581,24 +581,24 @@ private void fixWithAIAssist(ScanIssue issue) { .buildRemediationPrompt(issue); if (prompt == null || prompt.isEmpty()) { - System.out.println("[FINDINGS] ERROR: Failed to build remediation prompt"); + showErrorNotification("Failed to build prompt for this issue type"); return; } // Send to Copilot via integration - System.out.println("[FINDINGS] Sending prompt to Copilot..."); + boolean success = com.checkmarx.eclipse.devassist.ui.findings.integration.CopilotIntegration .sendPromptToCopilot(prompt); if (success) { - System.out.println("[FINDINGS] Prompt sent to Copilot successfully"); + } else { - System.out.println("[FINDINGS] ! Copilot not available, prompt in clipboard"); + } } catch (Exception e) { - System.out.println("[FINDINGS] ERROR: Exception in fixWithAIAssist: " + e.getMessage()); + e.printStackTrace(); showErrorNotification("Error: " + e.getMessage()); } @@ -630,23 +630,23 @@ private void ignoreThisFinding(ScanIssue issue) { return; } - System.out.println("[FINDINGS] IgnoredProblemsStore is initialized"); + // Add to ignored store with full finding details for display in Ignored Problems View ignoredStore.ignoreProblem(issue); - System.out.println("[FINDINGS] Added to IgnoredProblemsStore: " + issue.getScanIssueId()); + // Check if it was actually added boolean isIgnored = ignoredStore.isIgnored(issue.getScanIssueId()); // Refresh the tree to remove the ignored finding - System.out.println("[FINDINGS] Calling refreshTreeWithFilter..."); + refreshTreeWithFilter(); - System.out.println("[FINDINGS] ✓ Findings tree refreshed - finding removed"); + - System.out.println("[FINDINGS] ========================================"); + } catch (Exception e) { - System.err.println("[FINDINGS] ✗ Error ignoring finding: " + e.getMessage()); + System.err.println("[FINDINGS] ✗ Error ignoring finding: " + e.getMessage()); e.printStackTrace(); showErrorNotification("Failed to ignore finding: " + e.getMessage()); } @@ -679,12 +679,12 @@ private void ignoreAllOfType(ScanIssue issue) { } } - System.out.println("[FINDINGS] ✓ Ignored " + ignoredCount + " findings of this type"); + refreshTreeWithFilter(); - System.out.println("[FINDINGS] ✓ Findings tree refreshed"); - System.out.println("[FINDINGS] ========================================"); + + } catch (Exception e) { - System.err.println("[FINDINGS] ✗ Error ignoring findings of type: " + e.getMessage()); + System.err.println("[FINDINGS] ✗ Error ignoring findings of type: " + e.getMessage()); e.printStackTrace(); showErrorNotification("Failed to ignore findings of this type: " + e.getMessage()); } @@ -706,10 +706,10 @@ private void copyIssueDetails(ScanIssue issue) { try { java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(new java.awt.datatransfer.StringSelection(json.toString()), null); - System.out.println("[FINDINGS] Issue details copied to clipboard"); - System.out.println(json.toString()); + + } catch (Exception e) { - System.out.println("[FINDINGS] Failed to copy to clipboard: " + e.getMessage()); + } } @@ -720,20 +720,20 @@ private String escapeJson(String text) { private void openFileInEditor(String filePath, int lineNumber, ScanIssue issue) { try { - System.out.println("[FINDINGS] Attempting to navigate to: " + filePath + " at line " + lineNumber); + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( new org.eclipse.core.runtime.Path(filePath)); if (file == null || !file.exists()) { - System.out.println("✗ File not found in workspace: " + filePath); + return; } // 1. Open file in active workbench page IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); IEditorPart editor = IDE.openEditor(page, file); - System.out.println("✓ Opened file in editor: " + filePath); + // **CRITICAL FIX: Navigation-based opens don't trigger IPartListener2 events** // Directly set up real-time scanning and apply cached decorations @@ -750,7 +750,7 @@ private void openFileInEditor(String filePath, int lineNumber, ScanIssue issue) } } catch (Exception e) { - System.out.println("✗ Error navigating to file: " + e.getMessage()); + e.printStackTrace(); } } @@ -761,12 +761,12 @@ private void openFileInEditor(String filePath, int lineNumber, ScanIssue issue) */ private void setupRealtimeScanningForFile(org.eclipse.core.resources.IFile file, IEditorPart editor) { if (file == null || editor == null) { - System.out.println("[REALTIME-SETUP] ✗ File or editor is null"); + return; } - System.out.println("[REALTIME-SETUP] [STEP 1/5] Starting setup for: " + file.getName()); - System.out.println("[REALTIME-SETUP] [STEP 1/5] Editor type: " + editor.getClass().getSimpleName()); + + try { // Extract document for real-time scanning @@ -774,50 +774,50 @@ private void setupRealtimeScanningForFile(org.eclipse.core.resources.IFile file, String filePath = file.getLocation().toOSString(); String fileName = file.getName(); - System.out.println("[REALTIME-SETUP] [STEP 2/5] Extracting document from editor..."); + // Try method 1: Direct ITextEditor instance check if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { - System.out.println("[REALTIME-SETUP] [STEP 2/5] Editor is ITextEditor (direct)"); + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); } // Try method 2: ITextEditor Adapter pattern (for MavenPomEditor, etc.) if (document == null) { - System.out.println("[REALTIME-SETUP] [STEP 2/5] Trying ITextEditor adapter pattern..."); + org.eclipse.ui.texteditor.ITextEditor textEditor = editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); if (textEditor != null) { - System.out.println("[REALTIME-SETUP] [STEP 2/5] Got ITextEditor via adapter"); + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); } } // Try method 3: Direct IDocument adapter (some editors provide this directly) if (document == null) { - System.out.println("[REALTIME-SETUP] [STEP 2/5] Trying direct IDocument adapter..."); + document = editor.getAdapter(org.eclipse.jface.text.IDocument.class); if (document != null) { - System.out.println("[REALTIME-SETUP] [STEP 2/5] Got IDocument directly via adapter"); + } } if (document == null) { - System.out.println("[REALTIME-SETUP] ✗ [STEP 2/5] FAILED: Could not extract document from editor type: " + editor.getClass().getName()); + return; } - System.out.println("[REALTIME-SETUP] ✓ [STEP 2/5] Document extracted successfully"); + - System.out.println("[REALTIME-SETUP] [STEP 3/5] Creating RealTimeScanJob for: " + fileName); + // Create a scan job for this file com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob scanJob = new com.checkmarx.eclipse.devassist.ui.findings.realtime.RealTimeScanJob(file, fileName); - System.out.println("[REALTIME-SETUP] ✓ [STEP 3/5] RealTimeScanJob created"); + - System.out.println("[REALTIME-SETUP] [STEP 4/5] Creating and registering document listener..."); + // Create a document listener that reschedules the job on every keystroke com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = null; @@ -837,18 +837,18 @@ private void setupRealtimeScanningForFile(org.eclipse.core.resources.IFile file, // Register the document listener document.addDocumentListener(docListener); - System.out.println("[REALTIME-SETUP] ✓ [STEP 4/5] Document listener registered - edits will now trigger scans"); + - System.out.println("[REALTIME-SETUP] [STEP 5/5] Applying cached decorations..."); + // Apply cached decorations if findings exist for this file // Pass the editor directly to avoid search issues with MavenPomEditor applyCachedDecorationsForFile(file, document, editor); - System.out.println("[REALTIME-SETUP] ✓ [STEP 5/5] Setup complete for: " + fileName); + } catch (Exception e) { - System.err.println("[REALTIME-SETUP] ✗ EXCEPTION during setup: " + e.getMessage()); + 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(); @@ -886,12 +886,12 @@ private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file java.util.List cachedIssues = problemHolder.getScanIssuesByFile(filePath); if (cachedIssues == null || cachedIssues.isEmpty()) { - System.out.println("[REALTIME-SETUP] No cached findings for: " + file.getName()); + return; } // Apply decorations directly using the provided editor - System.out.println("[REALTIME-SETUP] ✓ Applying " + cachedIssues.size() + " cached decorations for: " + file.getName()); + applyDecorationsDirectly(editor, file, cachedIssues); } catch (Exception e) { @@ -917,14 +917,14 @@ private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, editor.getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); if (textEditor == null) { - System.out.println("[REALTIME-SETUP-DIRECT] ✗ Cannot adapt editor to ITextEditor"); + return; } // Get document provider and input org.eclipse.ui.texteditor.IDocumentProvider docProvider = textEditor.getDocumentProvider(); if (docProvider == null) { - System.out.println("[REALTIME-SETUP-DIRECT] ✗ No document provider for editor"); + return; } @@ -933,17 +933,17 @@ private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, docProvider.getAnnotationModel(textEditor.getEditorInput()); if (annotationModel == null) { - System.out.println("[REALTIME-SETUP-DIRECT] ✗ No annotation model from provider"); + return; } - System.out.println("[REALTIME-SETUP-DIRECT] ✓ Got annotation model, applying " + scanIssues.size() + " decorations"); + // Get document from provider org.eclipse.jface.text.IDocument document = docProvider.getDocument(textEditor.getEditorInput()); if (document == null) { - System.out.println("[REALTIME-SETUP-DIRECT] ✗ Cannot get document from provider"); + return; } @@ -967,17 +967,17 @@ private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, if (pos != null && pos.getLength() > 0) { annotationModel.addAnnotation(annotation, pos); annotations.add(annotation); - System.out.println("[REALTIME-SETUP-DIRECT] ✓ Added annotation for: " + issue.getTitle()); + } } catch (Exception e) { System.err.println("[REALTIME-SETUP-DIRECT] Error decorating issue: " + e.getMessage()); } } - System.out.println("[REALTIME-SETUP-DIRECT] ✓ Applied " + annotations.size() + " decorations successfully"); + } catch (Exception e) { - System.err.println("[REALTIME-SETUP-DIRECT] ✗ Error applying decorations directly: " + e.getMessage()); + System.err.println("[REALTIME-SETUP-DIRECT] ✗ Error applying decorations directly: " + e.getMessage()); e.printStackTrace(); } } @@ -1088,13 +1088,13 @@ private boolean scrollToLine(IEditorPart editor, int lineNumber) { // Line numbers in IDocument are 0-indexed int lineOffset = document.getLineOffset(lineNumber - 1); textEditor.selectAndReveal(lineOffset, 0); - System.out.println("[FINDINGS] ✓ Successfully scrolled to line " + lineNumber); + return true; } } } } catch (Exception e) { - System.out.println("[FINDINGS] Line scrolling via adapter failed: " + e.getMessage()); + } return false; } @@ -1129,7 +1129,7 @@ private void createMarkerForIssue(IFile file, ScanIssue issue) { // 3. Populate custom attributes com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); - System.out.println("[FINDINGS] ✓ Created marker with LINE_NUMBER=" + lineNumber + " and MESSAGE=" + issue.getTitle()); + } catch (org.eclipse.core.runtime.CoreException e) { System.err.println("[FINDINGS] Error creating marker: " + e.getMessage()); @@ -1155,12 +1155,12 @@ private void highlightViaMarker(org.eclipse.ui.IEditorPart editor, IFile file, S IMarker marker = findMarkerForIssue(file, issue); if (marker != null && marker.exists()) { org.eclipse.ui.ide.IDE.gotoMarker(editor, marker); - System.out.println("[FINDINGS] ✓ Navigated to marker for: " + issue.getTitle()); + } else { - System.out.println("[FINDINGS] No marker found for issue: " + issue.getTitle()); + } } catch (Exception e) { - System.out.println("[FINDINGS] Error navigating to marker: " + e.getMessage()); + } } @@ -1189,7 +1189,7 @@ private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { } } } catch (Exception e) { - System.out.println("[FINDINGS] Error finding marker for issue: " + e.getMessage()); + } return null; @@ -1212,23 +1212,23 @@ private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { */ // private void createMarkerForIssue(IFile file, ScanIssue issue) { // if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { -// System.out.println("[FINDINGS] [MARKER-CREATE] ✗ Missing file, issue, or locations"); +// // return; // } // // try { -// System.out.println("[FINDINGS] [MARKER-CREATE] ╔═══════════════════════════════════════╗"); -// System.out.println("[FINDINGS] [MARKER-CREATE] â•‘ Creating marker for ScanIssue â•‘"); -// System.out.println("[FINDINGS] [MARKER-CREATE] ╚═══════════════════════════════════════╝"); -// System.out.println("[FINDINGS] [MARKER-CREATE] File: " + file.getFullPath()); -// System.out.println("[FINDINGS] [MARKER-CREATE] Issue: " + issue.getTitle()); -// System.out.println("[FINDINGS] [MARKER-CREATE] Engine: " + issue.getScanEngine()); -// System.out.println("[FINDINGS] [MARKER-CREATE] Line: " + issue.getLocations().get(0).getLine()); +// +// +// +// +// +// +// // // // Step 1: Check if marker already exists for this issue // IMarker existingMarker = findMarkerForIssue(file, issue); // if (existingMarker != null && existingMarker.exists()) { -// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker already exists, skipping creation"); +// // return; // } // @@ -1238,12 +1238,12 @@ private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { // // - Marker appears in Eclipse's Problems View // // - Can be navigated with IDE.gotoMarker() // IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); -// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker created"); +// // // // Step 3: Populate marker attributes using MarkerIssueMapper // // This stores all ScanIssue data in marker for later retrieval // com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); -// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker populated with issue data"); +// // // // Step 4: Verify marker creation // if (newMarker.exists()) { @@ -1251,21 +1251,21 @@ private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { // int markerLine = newMarker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); // int markerSeverity = newMarker.getAttribute(org.eclipse.core.resources.IMarker.SEVERITY, -1); // -// System.out.println("[FINDINGS] [MARKER-CREATE] ✓ Marker verified:"); -// System.out.println("[FINDINGS] [MARKER-CREATE] ID: " + newMarker.getId()); -// System.out.println("[FINDINGS] [MARKER-CREATE] Message: " + markerMsg); -// System.out.println("[FINDINGS] [MARKER-CREATE] Line: " + markerLine); -// System.out.println("[FINDINGS] [MARKER-CREATE] Severity: " + markerSeverity); -// System.out.println("[FINDINGS] [MARKER-CREATE] ═════════════════════════════════════════"); +// +// +// +// +// +// // } else { -// System.out.println("[FINDINGS] [MARKER-CREATE] ✗ Failed to create marker!"); +// // } // // } catch (org.eclipse.core.runtime.CoreException e) { -// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ CoreException creating marker: " + e.getMessage()); +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ CoreException creating marker: " + e.getMessage()); // e.printStackTrace(); // } catch (Exception e) { -// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ Error creating marker: " + e.getMessage()); +// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ Error creating marker: " + e.getMessage()); // e.printStackTrace(); // } // } @@ -1273,7 +1273,7 @@ private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { private void showContextMenu(MouseEvent e) { ISelection selection = treeViewer.getSelection(); if (!(selection instanceof IStructuredSelection)) { - System.out.println("[FINDINGS] Invalid selection for context menu"); + return; } @@ -1281,14 +1281,14 @@ private void showContextMenu(MouseEvent e) { Object element = ssel.getFirstElement(); if (!(element instanceof ScanDetailWithPath)) { - System.out.println("[FINDINGS] Context menu: Selected element is not a ScanDetailWithPath"); + return; } ScanDetailWithPath detailWithPath = (ScanDetailWithPath) element; ScanIssue issue = detailWithPath.getDetail(); - System.out.println("[FINDINGS] Creating context menu for: " + issue.getTitle()); + org.eclipse.swt.widgets.Menu menu = new org.eclipse.swt.widgets.Menu(treeViewer.getTree()); @@ -1298,7 +1298,7 @@ private void showContextMenu(MouseEvent e) { viewDetailsItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: View Details - Issue: " + issue.getTitle()); + showIssueDetails(issue); } }); @@ -1309,7 +1309,7 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { fixWithAIItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: Fix with AI Assist - Issue: " + issue.getTitle()); + fixWithAIAssist(issue); } }); @@ -1320,7 +1320,7 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { ignoreItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: Ignore This Finding - Issue: " + issue.getTitle()); + ignoreThisFinding(issue); } }); @@ -1332,7 +1332,7 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { ignoreAllItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: Ignore All of This Type - Issue Type: " + issue.getTitle()); + ignoreAllOfType(issue); } }); @@ -1347,7 +1347,7 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { copyItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: Copy Issue Details - Issue: " + issue.getTitle()); + copyIssueDetails(issue); } }); @@ -1358,7 +1358,7 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { terminalItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - System.out.println("[FINDINGS] Action: Navigate to Line - File: " + detailWithPath.getFilePath()); + navigateToIssue(detailWithPath); } }); @@ -1368,15 +1368,13 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { } private void refreshTreeWithFilter() { - System.out.println("[FINDINGS] ========== REFRESH TREE START =========="); - System.out.println("[FINDINGS] Refreshing tree with active filters and ignored problems..."); - System.out.println("[FINDINGS] Current issues: " + currentIssues.size() + " files"); + + + // Apply active filters and refresh VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); - System.out.println("[FINDINGS] Active filters: " + filterState.getFilters()); - System.out.println("[FINDINGS] Ignored IDs in store: " + - (ignoredStore != null ? ignoredStore.getIgnoredProblemIds() : "[]")); + Map> filteredIssues = new HashMap<>(); int totalBefore = 0; @@ -1390,9 +1388,9 @@ private void refreshTreeWithFilter() { List filtered = new java.util.ArrayList<>(); for (ScanIssue issue : issues) { - // ✅ Safe null guard FIRST before calling any methods on issue + // ✅ Safe null guard FIRST before calling any methods on issue if (issue == null || issue.getSeverity() == null) { - System.out.println("[FINDINGS] WARNING: Null issue or severity detected"); + continue; } @@ -1401,29 +1399,25 @@ private void refreshTreeWithFilter() { boolean hasFilter = filterState.hasFilter(issue.getSeverity()); boolean isProblem = com.checkmarx.eclipse.devassist.backend.DevAssistUtils.isProblem(issue.getSeverity()); - System.out.println("[FINDINGS] Issue: " + issue.getTitle() + - " | ID: " + issueId + - " | Ignored: " + isIgnored + - " | HasFilter: " + hasFilter + - " | IsProblem: " + isProblem); + // Filter by OK/UNKNOWN/IGNORED severity (Phase 3) if (!isProblem) { - System.out.println("[FINDINGS] -> Filtered out because severity is OK/UNKNOWN/IGNORED"); + continue; } // Filter by severity preference if (!hasFilter) { - System.out.println("[FINDINGS] -> Filtered out by severity"); + continue; } // Filter out ignored problems if (isIgnored) { - System.out.println("[FINDINGS] -> Filtered out because IGNORED"); + continue; } - System.out.println("[FINDINGS] -> KEEPING"); + filtered.add(issue); } @@ -1433,19 +1427,19 @@ private void refreshTreeWithFilter() { } } - System.out.println("[FINDINGS] Total issues before filtering: " + totalBefore); - System.out.println("[FINDINGS] Total issues after filtering: " + totalAfter); - System.out.println("[FINDINGS] Filtered issues map: " + filteredIssues.size() + " files"); + + + - // ✅ Verify treeViewer control before manipulating UI + // ✅ Verify treeViewer control before manipulating UI if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { - System.out.println("[FINDINGS] Setting tree input..."); + treeViewer.setInput(filteredIssues); - System.out.println("[FINDINGS] Expanding all nodes..."); + treeViewer.expandAll(); } - System.out.println("[FINDINGS] ========== REFRESH TREE END =========="); + } /** @@ -1456,12 +1450,12 @@ private void refreshTreeWithFilter() { public void refreshTree(Map> issues) { if (issues == null) return; - System.out.println("[FINDINGS] ╔════════════════════════════════════════════╗"); - System.out.println("[FINDINGS] â•‘ FINDINGS VIEW: REFRESH TREE â•‘"); - System.out.println("[FINDINGS] ╚════════════════════════════════════════════╝"); - System.out.println("[FINDINGS] Input: " + issues.size() + " files"); + + + + int totalIssues = issues.values().stream().filter(java.util.Objects::nonNull).mapToInt(List::size).sum(); - System.out.println("[FINDINGS] Total Issues: " + totalIssues); + // Log issues by severity Map severityCounts = new HashMap<>(); @@ -1476,27 +1470,23 @@ public void refreshTree(Map> issues) { } }); - System.out.println("[FINDINGS] Severity breakdown:"); - severityCounts.forEach((severity, count) -> - System.out.println("[FINDINGS] - " + severity + ": " + count) - ); + for (String filePath : issues.keySet()) { List fileIssues = issues.get(filePath); - System.out.println("[FINDINGS] File: " + filePath + " → " + (fileIssues != null ? fileIssues.size() : 0) + " issues"); } - System.out.println("[FINDINGS] Setting currentIssues and dispatching UI update..."); + this.currentIssues = issues; - // ✅ Thread-safe dispatching for background updates + // ✅ Thread-safe dispatching for background updates org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { refreshTreeWithFilter(); } }); - System.out.println("[FINDINGS] ════════════════════════════════════════════"); + } @Override @@ -1515,7 +1505,7 @@ public TreeViewer getTreeViewer() { */ @Override public void onIgnoredProblemsChanged() { - System.out.println("[FINDINGS] Ignored problems changed - refreshing findings tree"); + if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { treeViewer.getControl().getDisplay().asyncExec(this::refreshTreeWithFilter); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java index d8cf09fb..9e2b7d21 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterState.java @@ -45,7 +45,7 @@ public void removeFilter(String severity) { public boolean hasFilter(String severity) { if (severity == null) { - System.out.println("[FILTER] WARNING: Null severity passed to hasFilter"); + return false; } boolean result = selectedFilters.contains(severity.toLowerCase()); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java index 5004d8da..f3bd7843 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.editor; +package com.checkmarx.eclipse.devassist.ui.findings.editor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; @@ -44,13 +44,13 @@ public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); if (viewer == null) { - System.out.println("[FINDINGS-OVERLAY] Could not get source viewer from editor"); + return; } IDocument document = viewer.getDocument(); if (document == null || lineNumber < 0 || lineNumber >= document.getNumberOfLines()) { - System.out.println("[FINDINGS-OVERLAY] Invalid document or line number: " + lineNumber); + return; } @@ -68,14 +68,14 @@ public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { IAnnotationModel annotationModel = viewer.getAnnotationModel(); if (annotationModel != null) { annotationModel.addAnnotation(annotation, position); - System.out.println("Annotation added"); - System.out.println("Annotation model = " + annotationModel.getClass().getName()); - System.out.println("Annotation type = " + annotation.getType()); - System.out.println("Offset = " + position.offset); - System.out.println("Length = " + position.length); + + + + + } } catch (BadLocationException e) { - System.out.println("[FINDINGS-OVERLAY] Error highlighting line: " + e.getMessage()); + } } @@ -105,9 +105,9 @@ public static void clearHighlights(TextEditor editor) { } }); - System.out.println("[FINDINGS-OVERLAY] ✓ Cleared all findings highlights"); + } catch (Exception e) { - System.out.println("[FINDINGS-OVERLAY] Error clearing highlights: " + e.getMessage()); + } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java index 558f67cd..a0b9f788 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java @@ -29,7 +29,7 @@ public static Image createFullCompositeImage(FileNodeLabel fileNode) { Display display = Display.getDefault(); Image compositeImage = createFullBadgeImage(display, fileNode); if (compositeImage != null) { - System.out.println("[SEVERITY-COMPOSER] Created full composite image with severity icons"); + } return compositeImage; } catch (Exception e) { @@ -144,7 +144,7 @@ private static Image createBadgeImage(Display display, FileNodeLabel fileNode) { } gc.dispose(); - System.out.println("[SEVERITY-COMPOSER] Created composite image: " + width + "x" + iconSize); + return compositeImage; } catch (Exception e) { @@ -220,7 +220,7 @@ private static Image createFullBadgeImage(Display display, FileNodeLabel fileNod } gc.dispose(); - System.out.println("[SEVERITY-COMPOSER] Created full badge image: " + totalWidth + "x" + iconSize); + return compositeImage; } catch (Exception e) { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java index e4404b00..94bd402d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/ignored/IgnoredProblemsStore.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.ignored; +package com.checkmarx.eclipse.devassist.ui.findings.ignored; import java.util.ArrayList; import java.util.Collections; @@ -42,7 +42,7 @@ public static IgnoredProblemsStore getInstance() { */ public void ignoreProblem(String problemId) { if (problemId != null && ignoredProblemIds.add(problemId)) { - System.out.println("[IGNORED-STORE] Added to ignored: " + problemId); + saveToPreferences(); notifyListeners(); } @@ -54,13 +54,13 @@ public void ignoreProblem(String problemId) { */ public void ignoreProblem(ScanIssue issue) { if (issue != null && issue.getScanIssueId() != null) { - System.out.println("[IGNORED-STORE] ignoreProblem(ScanIssue) called with ID: " + issue.getScanIssueId()); + ignoreProblem(issue.getScanIssueId()); // Cache the full issue details for later retrieval ignoredProblemsCache.put(issue.getScanIssueId(), issue); - System.out.println("[IGNORED-STORE] ✓ Cached issue details. Cache size: " + ignoredProblemsCache.size()); + } else { - System.out.println("[IGNORED-STORE] ✗ ERROR: issue is null or ID is null!"); + } } @@ -69,7 +69,7 @@ public void ignoreProblem(ScanIssue issue) { */ public void restoreProblem(String problemId) { if (problemId != null && ignoredProblemIds.remove(problemId)) { - System.out.println("[IGNORED-STORE] Removed from ignored: " + problemId); + ignoredProblemsCache.remove(problemId); saveToPreferences(); notifyListeners(); @@ -150,7 +150,7 @@ public void clearAll() { ignoredProblemsCache.clear(); saveToPreferences(); notifyListeners(); - System.out.println("[IGNORED-STORE] Cleared all ignored problems"); + } /** @@ -186,7 +186,7 @@ private void loadFromPreferences() { ignoredProblemIds.add(id.trim()); } } - System.out.println("[IGNORED-STORE] Loaded " + ignoredProblemIds.size() + " ignored problems from preferences"); + } } catch (Exception e) { System.err.println("[IGNORED-STORE] Error loading preferences: " + e.getMessage()); @@ -199,7 +199,7 @@ private void saveToPreferences() { String ignored = String.join(SEPARATOR, ignoredProblemIds); prefs.put(PREF_IGNORED_PROBLEMS, ignored); prefs.flush(); - System.out.println("[IGNORED-STORE] Saved " + ignoredProblemIds.size() + " ignored problems to preferences"); + } catch (BackingStoreException e) { System.err.println("[IGNORED-STORE] Error saving preferences: " + e.getMessage()); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java index a1b88bda..bb3aa862 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/integration/CopilotIntegration.java @@ -252,7 +252,7 @@ private static boolean executeOpenCopilotCommand(String prompt) { // Execute the command with parameters try { - System.out.println("=== BEFORE EXECUTE ==="); + command.executeWithChecks(new ExecutionEvent( command, parameters, @@ -260,13 +260,13 @@ private static boolean executeOpenCopilotCommand(String prompt) { null )); - System.out.println("=== AFTER EXECUTE ==="); + CxLogger.info(LOG_PREFIX + " ✓ Successfully executed Copilot command with prompt"); success[0] = true; - System.out.println("=== SUCCESS SET TRUE ==="); + } catch (Exception e) { CxLogger.warning(LOG_PREFIX + " Command execution failed: " + e.getMessage()); @@ -281,7 +281,7 @@ private static boolean executeOpenCopilotCommand(String prompt) { } catch (Exception e) { CxLogger.error(LOG_PREFIX + " Error in executeOpenCopilotCommand: " + e.getMessage(), e); } - System.out.println("executeOpenCopilotCommand returning = " + success[0]); + return success[0]; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java index 1956b594..93e33774 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/marker/MarkerIssueMapper.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.marker; +package com.checkmarx.eclipse.devassist.ui.findings.marker; import org.eclipse.core.resources.IMarker; @@ -81,7 +81,7 @@ public static ScanIssue fromMarker(IMarker marker) { return issue; } catch (Exception e) { - System.out.println("[MARKER-MAPPER] Error reconstructing ScanIssue from marker: " + e.getMessage()); + e.printStackTrace(); return null; } @@ -142,10 +142,10 @@ public static void populateMarker(IMarker marker, ScanIssue issue) { marker.setAttribute(IMarker.SEVERITY, severity); } - System.out.println("[MARKER-MAPPER] Populated marker for issue: " + issue.getTitle()); + } catch (Exception e) { - System.out.println("[MARKER-MAPPER] Error populating marker: " + e.getMessage()); + e.printStackTrace(); } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java index 9d81893a..434ae8da 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.provider; +package com.checkmarx.eclipse.devassist.ui.findings.provider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; @@ -31,9 +31,9 @@ public Object[] getElements(Object inputElement) { @SuppressWarnings("unchecked") Map> map = (Map>) inputElement; - System.out.println("[FINDINGS-CONTENT] ========================================"); - System.out.println("[FINDINGS-CONTENT] Creating FileNodeLabel elements..."); - System.out.println("[FINDINGS-CONTENT] Input files: " + map.size()); + + + Object[] elements = map.entrySet().stream() .map(entry -> { @@ -41,9 +41,9 @@ public Object[] getElements(Object inputElement) { Image fileIcon = getFileIcon(fileName); List issues = entry.getValue(); - System.out.println("[FINDINGS-CONTENT] File: " + fileName); - System.out.println("[FINDINGS-CONTENT] Path: " + entry.getKey()); - System.out.println("[FINDINGS-CONTENT] Issues: " + issues.size()); + + + // Calculate and log severity counts java.util.Map counts = new java.util.HashMap<>(); @@ -51,9 +51,6 @@ public Object[] getElements(Object inputElement) { String severity = issue.getSeverity(); counts.put(severity, counts.getOrDefault(severity, 0L) + 1); } - counts.forEach((sev, cnt) -> - System.out.println("[FINDINGS-CONTENT] " + sev + ": " + cnt) - ); return new FileNodeLabel( fileName, @@ -63,13 +60,11 @@ public Object[] getElements(Object inputElement) { }) .toArray(); - System.out.println("[FINDINGS-CONTENT] ✓ Created " + elements.length + " FileNodeLabel elements"); - System.out.println("[FINDINGS-CONTENT] ========================================"); + + return elements; } - System.out.println("[FINDINGS-CONTENT] ✗ Input is not a Map, type: " + - (inputElement != null ? inputElement.getClass().getSimpleName() : "null")); return new Object[0]; } @@ -89,7 +84,7 @@ private Image getFileIcon(String fileName) { } } } catch (Exception e) { - System.out.println("[FINDINGS-CONTENT] Error getting file icon for: " + fileName + " - " + e.getMessage()); + } return null; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java index f3d31a70..6fd14af3 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxEditorListener.java @@ -49,7 +49,7 @@ public class CheckmarxEditorListener implements IPartListener2 { private final Map activeScanJobs = new HashMap<>(); public CheckmarxEditorListener() { - System.out.println("[REALTIME] ✓ CheckmarxEditorListener created"); + } /** @@ -93,7 +93,7 @@ public void partActivated(IWorkbenchPartReference partRef) { if (activeListeners.containsKey(documentId)) { RealTimeScanJob scanJob = activeScanJobs.get(documentId); if (scanJob != null) { - System.out.println("[REALTIME] User switched to tab - triggering rescan for: " + extractFileNameFromEditor(editor)); + scanJob.reschedule(0); } return; @@ -147,13 +147,13 @@ private void setupRealtimeScanning(IEditorPart editor) { // Check if we've already set up scanning for this document if (activeListeners.containsKey(documentId)) { - System.out.println("[REALTIME] Document listener already registered"); + return; } // Get file name for logging String fileName = extractFileNameFromEditor(editor); - System.out.println("[REALTIME] Setting up real-time scanning for: " + fileName); + // Log to Eclipse Error Log String message = "User opened the file: " + fileName; @@ -175,7 +175,7 @@ private void setupRealtimeScanning(IEditorPart editor) { new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "scan-scheduler")); } } catch (Exception e) { - System.out.println("[REALTIME] Warning: Could not get scheduler from session: " + e.getMessage()); + } } @@ -190,7 +190,7 @@ private void setupRealtimeScanning(IEditorPart editor) { activeListeners.put(documentId, docListener); activeScanJobs.put(documentId, scanJob); - System.out.println("[REALTIME] ✓ Document listener registered for: " + fileName); + // **CRITICAL FIX: Apply cached decorations if findings exist for this file** // JetBrains pattern: when editor opens, apply cached decorations immediately @@ -200,7 +200,7 @@ private void setupRealtimeScanning(IEditorPart editor) { // **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 - System.out.println("[REALTIME] Triggering initial scan for: " + fileName); + scanJob.reschedule(0); } catch (Exception e) { @@ -233,7 +233,7 @@ private void cleanupRealtimeScanning(IEditorPart editor) { try { document.removeDocumentListener(listener); listener.dispose(); - System.out.println("[REALTIME] ✓ Document listener removed for: " + listener.getFileName()); + } catch (Exception e) { System.err.println("[REALTIME] Error removing document listener: " + e.getMessage()); } @@ -243,7 +243,7 @@ private void cleanupRealtimeScanning(IEditorPart editor) { RealTimeScanJob scanJob = activeScanJobs.remove(documentId); if (scanJob != null) { scanJob.cancel(); - System.out.println("[REALTIME] ✓ Scan job cancelled for: " + scanJob.getFileName()); + } } @@ -361,12 +361,12 @@ private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file problemHolder.getScanIssuesByFile(filePath); if (cachedIssues == null || cachedIssues.isEmpty()) { - System.out.println("[REALTIME] No cached findings for: " + file.getName()); + return; } // Apply decorations for cached findings - System.out.println("[REALTIME] ✓ Applying " + cachedIssues.size() + " cached decorations for: " + file.getName()); + ProblemDecorator.decorateEditor(file, cachedIssues); } catch (Exception e) { diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java index 55ece6e3..65f4e262 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java @@ -45,7 +45,7 @@ public RealTimeScanJob(IFile file, String fileName) { setPriority(Job.DECORATE); // Lower priority than user interactions setUser(false); // Not a user-initiated job - System.out.println("[REALTIME] ✓ RealTimeScanJob created for: " + fileName); + } /** @@ -73,7 +73,7 @@ public synchronized void reschedule(long delayMs) { // Schedule the job to run after the delay schedule(delayMs); - System.out.println("[REALTIME] Job rescheduled for: " + fileName + " (delay=" + delayMs + "ms)"); + } /** @@ -96,34 +96,33 @@ protected IStatus run(IProgressMonitor monitor) { try { // Check if file still exists and is accessible if (file == null || !file.exists()) { - System.out.println("[REALTIME] ✗ File no longer exists: " + fileName); + return Status.CANCEL_STATUS; } // Check if the job was cancelled while waiting if (monitor.isCanceled()) { - System.out.println("[REALTIME] ✗ Scan cancelled for: " + fileName); + return Status.CANCEL_STATUS; } // **STEP 1: Check authentication status** if (!isUserAuthenticated()) { - System.out.println("[REALTIME] ✗ BLOCKED: User not authenticated - scan cannot proceed"); - System.out.println("[REALTIME] ℹ️ User must configure API key in preferences first"); + + return Status.OK_STATUS; // Return OK but don't scan } - System.out.println("[REALTIME] ════════════════════════════════════════"); - System.out.println("[REALTIME] ✓ Authentication verified - starting backend security scan..."); - System.out.println("[REALTIME] File: " + fileName); - System.out.println("[REALTIME] Last change: " + (System.currentTimeMillis() - lastChangeTime) + "ms ago"); - System.out.println("[REALTIME] ════════════════════════════════════════"); + + + + // Call our backend scanners via ScanManager try { org.eclipse.core.resources.IProject project = file.getProject(); if (project == null || !project.isOpen()) { - System.out.println("[REALTIME] ✗ Project not accessible"); + return Status.OK_STATUS; } @@ -144,44 +143,42 @@ protected IStatus run(IProgressMonitor monitor) { // Lazy initialization if not found if (registry == null) { - System.out.println("[REALTIME] [STEP 1/5] Lazily initializing ScannerRegistry for: " + projectName); + registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project); registry.registerAllScanners(); project.setSessionProperty(registryKey, registry); - System.out.println("[REALTIME] ✓ ScannerRegistry initialized"); + } if (stateHolder == null) { - System.out.println("[REALTIME] [STEP 2/5] Lazily initializing DevAssistScanStateHolder for: " + projectName); + stateHolder = new com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder(); project.setSessionProperty(stateHolderKey, stateHolder); - System.out.println("[REALTIME] ✓ State holder initialized"); + } // Execute backend scanners - System.out.println("[REALTIME] [STEP 3/5] Creating ScanManager..."); + com.checkmarx.eclipse.devassist.common.ScanManager scanManager = new com.checkmarx.eclipse.devassist.common.ScanManager(registry, stateHolder); String filePath = file.getLocation().toOSString(); - System.out.println("[REALTIME] [STEP 4/5] Executing backend scanners for: " + filePath); + java.util.List issues = scanManager.scanFile(filePath); - System.out.println("[REALTIME] ✓ Scan completed - found " + issues.size() + " issues"); + for (com.checkmarx.eclipse.devassist.model.ScanIssue issue : issues) { - System.out.println("[REALTIME] - " + issue.getScanEngine() + ": " + issue.getTitle() + - " (severity: " + issue.getSeverity() + ")"); } // Publish results to UI - System.out.println("[REALTIME] [STEP 5/5] Publishing results to UI..."); + if (!issues.isEmpty()) { com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues); - System.out.println("[REALTIME] ✓ Results successfully published to findings view"); + } else { - System.out.println("[REALTIME] ℹ️ No issues found - findings view will be empty for this file"); + } } catch (Exception e) { @@ -193,7 +190,7 @@ protected IStatus run(IProgressMonitor monitor) { } } - System.out.println("[REALTIME] ════════════════════════════════════════"); + return Status.OK_STATUS; } catch (Exception e) { @@ -230,7 +227,7 @@ public boolean belongsTo(Object family) { */ @Override protected void canceling() { - System.out.println("[REALTIME] Cancelling scan for: " + fileName); + super.canceling(); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java index 145cd157..7881693f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/resolution/ViewFindingDetailsResolution.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.resolution; +package com.checkmarx.eclipse.devassist.ui.findings.resolution; import org.eclipse.core.resources.IMarker; import org.eclipse.jface.dialogs.Dialog; @@ -57,7 +57,7 @@ public void run(IMarker marker) { // Reconstruct ScanIssue from marker attributes ScanIssue issue = MarkerIssueMapper.fromMarker(marker); if (issue == null) { - System.out.println("[CX-RESOLUTION] Failed to reconstruct ScanIssue from marker"); + return; } @@ -68,10 +68,10 @@ public void run(IMarker marker) { ); dialog.open(); - System.out.println("[CX-RESOLUTION] Opened finding details: " + issue.getTitle()); + } catch (Exception e) { - System.out.println("[CX-RESOLUTION] Error opening finding details: " + e.getMessage()); + e.printStackTrace(); } } @@ -152,7 +152,7 @@ protected Control createDialogArea(Composite parent) { // Quick Fix button Button quickFixBtn = new Button(buttonsComposite, SWT.PUSH); - quickFixBtn.setText("âš¡ Quick Fix"); + quickFixBtn.setText("⚡ Quick Fix"); quickFixBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); quickFixBtn.addSelectionListener(new SelectionAdapter() { @Override @@ -163,7 +163,7 @@ public void widgetSelected(SelectionEvent e) { // Ignore button Button ignoreBtn = new Button(buttonsComposite, SWT.PUSH); - ignoreBtn.setText("🚫 Ignore"); + ignoreBtn.setText("🚫 Ignore"); ignoreBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); ignoreBtn.addSelectionListener(new SelectionAdapter() { @Override @@ -174,7 +174,7 @@ public void widgetSelected(SelectionEvent e) { // Copy button Button copyBtn = new Button(buttonsComposite, SWT.PUSH); - copyBtn.setText("📋 Copy"); + copyBtn.setText("📋 Copy"); copyBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); copyBtn.addSelectionListener(new SelectionAdapter() { @Override @@ -185,7 +185,7 @@ public void widgetSelected(SelectionEvent e) { // Open Window button Button openBtn = new Button(buttonsComposite, SWT.PUSH); - openBtn.setText("🪟 Details"); + openBtn.setText("🪟 Details"); openBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); openBtn.addSelectionListener(new SelectionAdapter() { @Override @@ -204,12 +204,12 @@ protected void createButtonsForButtonBar(Composite parent) { } private void onQuickFixClick() { - System.out.println("[FINDING-DETAILS] Quick Fix clicked for: " + issue.getTitle()); + // TODO: Implement remediation integration } private void onIgnoreClick() { - System.out.println("[FINDING-DETAILS] Ignore clicked for: " + issue.getTitle()); + // TODO: Implement ignore logic } @@ -223,30 +223,30 @@ private void onCopyClick() { TextTransfer transfer = TextTransfer.getInstance(); clipboard.setContents(new Object[] { text }, new Transfer[] { transfer }); clipboard.dispose(); - System.out.println("[FINDING-DETAILS] ✓ Copied to clipboard"); + }); } private void onOpenWindowClick() { - System.out.println("[FINDING-DETAILS] Open Findings Window clicked for: " + issue.getTitle()); + // TODO: Open Findings window and navigate to this issue } private String getSeverityIcon(String severity) { if (severity == null) { - return "⚪"; + return "⚪"; } switch (severity.toLowerCase()) { case "critical": - return "🔴"; + return "🔴"; case "high": - return "🟠"; + return "🟠"; case "medium": - return "🟡"; + return "🟡"; case "low": - return "🟢"; + return "🟢"; default: - return "⚪"; + return "⚪"; } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java index 50f6ff23..3ccf556e 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java @@ -52,7 +52,7 @@ public static String convertStringTimeStamp(String timestamp) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN).withZone(ZoneId.systemDefault()); parsedDate = dateTimeFormatter.format(instant); } catch (Exception e) { - System.out.println(e); + return timestamp; } From 0bfdc1d180d8d95e4cbd6dac3ab43ef796c40201 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Wed, 5 Aug 2026 10:38:31 +0530 Subject: [PATCH 5/9] Rebase --- checkmarx-ast-eclipse-plugin-tests/.classpath | 6 +++--- checkmarx-ast-eclipse-plugin/.classpath | 20 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin-tests/.classpath b/checkmarx-ast-eclipse-plugin-tests/.classpath index 98ee5fe7..13b02eb1 100644 --- a/checkmarx-ast-eclipse-plugin-tests/.classpath +++ b/checkmarx-ast-eclipse-plugin-tests/.classpath @@ -1,8 +1,7 @@ - + - @@ -14,7 +13,8 @@ - + + diff --git a/checkmarx-ast-eclipse-plugin/.classpath b/checkmarx-ast-eclipse-plugin/.classpath index 32e2245e..19a4d9ec 100644 --- a/checkmarx-ast-eclipse-plugin/.classpath +++ b/checkmarx-ast-eclipse-plugin/.classpath @@ -2,30 +2,26 @@ - + + + + + + - + + - - - - - - - - - - From 0309b8e85354afc9707a43d94139fa5c9df03c37 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Wed, 5 Aug 2026 15:58:14 +0530 Subject: [PATCH 6/9] Problems count --- .../ui/findings/icons/IconRegistry.java | 19 ++---------- .../findings/icons/SeverityImageComposer.java | 12 ------- .../ui/findings/model/FileNodeLabel.java | 3 +- .../provider/FindingsContentProvider.java | 29 ++--------------- .../provider/FindingsLabelProvider.java | 31 ++++++++++--------- .../realtime/CheckmarxDocumentListener.java | 19 ++++++++++++ 6 files changed, 42 insertions(+), 71 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java index aab54039..a5833cd1 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/IconRegistry.java @@ -33,10 +33,7 @@ public enum Severity { CRITICAL("critical"), HIGH("high"), MEDIUM("medium"), - LOW("low"), - UNKNOWN("unknown"), - OK("ok"), - IGNORED("ignored"); + LOW("low"); private final String name; @@ -66,9 +63,6 @@ private static void initializeRegistry() { registerIcon("high_16", "icons/severity/high_16.svg"); registerIcon("medium_16", "icons/severity/medium_16.svg"); registerIcon("low_16", "icons/severity/low_16.svg"); - registerIcon("unknown_16", "icons/severity/unknown_16.svg"); - registerIcon("ok_16", "icons/severity/ok_16.svg"); - registerIcon("ignored_16", "icons/severity/ignored_16.svg"); // Register medium icons (20px) registerIcon("malicious_20", "icons/severity/malicious_20.svg"); @@ -76,9 +70,6 @@ private static void initializeRegistry() { registerIcon("high_20", "icons/severity/high_20.svg"); registerIcon("medium_20", "icons/severity/medium_20.svg"); registerIcon("low_20", "icons/severity/low_20.svg"); - registerIcon("unknown_20", "icons/severity/unknown_20.svg"); - registerIcon("ok_20", "icons/severity/ok_20.svg"); - registerIcon("ignored_20", "icons/severity/ignored_20.svg"); // Register base icons registerIcon("malicious", "icons/severity/malicious.svg"); @@ -86,9 +77,6 @@ private static void initializeRegistry() { registerIcon("high", "icons/severity/high.svg"); registerIcon("medium", "icons/severity/medium.svg"); registerIcon("low", "icons/severity/low.svg"); - registerIcon("unknown", "icons/severity/unknown.svg"); - registerIcon("ok", "icons/severity/ok.svg"); - registerIcon("ignored", "icons/severity/ignored.svg"); } private static void registerIcon(String key, String path) { @@ -98,7 +86,6 @@ private static void registerIcon(String key, String path) { /** * Get icon for a severity level and size. - * Normalizes severity to match SeverityLevel enum values, then converts to lowercase for icon lookup. * * @param severity Severity level (case-insensitive) * @param size Icon size @@ -109,9 +96,7 @@ public static Image getIcon(String severity, Size size) { return null; } - // Normalize severity to SeverityLevel format, then convert to lowercase for icon key - String normalized = com.checkmarx.eclipse.devassist.backend.DevAssistUtils.normalizeSeverity(severity); - String key = normalized.toLowerCase() + size.getSuffix(); + String key = severity.toLowerCase() + size.getSuffix(); return imageRegistry.get(key); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java index a0b9f788..980648f1 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/icons/SeverityImageComposer.java @@ -28,13 +28,8 @@ public static Image createFullCompositeImage(FileNodeLabel fileNode) { try { Display display = Display.getDefault(); Image compositeImage = createFullBadgeImage(display, fileNode); - if (compositeImage != null) { - - } return compositeImage; } catch (Exception e) { - System.err.println("[SEVERITY-COMPOSER] Error creating full composite image: " + e.getMessage()); - e.printStackTrace(); return null; } } @@ -68,7 +63,6 @@ public static Image createSeverityBadgeImage(FileNodeLabel fileNode) { return compositeImage; } catch (Exception e) { - System.err.println("[SEVERITY-COMPOSER] Error creating composite image: " + e.getMessage()); return null; } } @@ -144,12 +138,9 @@ private static Image createBadgeImage(Display display, FileNodeLabel fileNode) { } gc.dispose(); - return compositeImage; } catch (Exception e) { - System.err.println("[SEVERITY-COMPOSER] Error creating badge image: " + e.getMessage()); - e.printStackTrace(); return null; } } @@ -220,12 +211,9 @@ private static Image createFullBadgeImage(Display display, FileNodeLabel fileNod } gc.dispose(); - return compositeImage; } catch (Exception e) { - System.err.println("[SEVERITY-COMPOSER] Error creating full badge image: " + e.getMessage()); - e.printStackTrace(); return null; } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java index 2f9da0d2..8baf4e55 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java @@ -48,7 +48,8 @@ private static Map calculateProblemCount(List issues) { for (ScanIssue issue : issues) { String severity = issue.getSeverity(); if (severity != null) { - counts.put(severity, counts.getOrDefault(severity, 0L) + 1); + String normalizedSeverity = severity.toLowerCase(); + counts.put(normalizedSeverity, counts.getOrDefault(normalizedSeverity, 0L) + 1); } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java index 434ae8da..7eaa61de 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsContentProvider.java @@ -30,41 +30,18 @@ public Object[] getElements(Object inputElement) { if (inputElement instanceof Map) { @SuppressWarnings("unchecked") Map> map = (Map>) inputElement; - - - - - - Object[] elements = map.entrySet().stream() + return map.entrySet().stream() .map(entry -> { String fileName = getFileName(entry.getKey()); Image fileIcon = getFileIcon(fileName); - List issues = entry.getValue(); - - - - - - // Calculate and log severity counts - java.util.Map counts = new java.util.HashMap<>(); - for (ScanIssue issue : issues) { - String severity = issue.getSeverity(); - counts.put(severity, counts.getOrDefault(severity, 0L) + 1); - } - return new FileNodeLabel( fileName, entry.getKey(), - issues, + entry.getValue(), fileIcon); }) .toArray(); - - - - return elements; } - return new Object[0]; } @@ -84,7 +61,6 @@ private Image getFileIcon(String fileName) { } } } catch (Exception e) { - } return null; @@ -134,4 +110,3 @@ public void dispose() { // Cleanup if needed } } - diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java index bc003450..0d462a32 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/provider/FindingsLabelProvider.java @@ -1,4 +1,4 @@ -package com.checkmarx.eclipse.devassist.ui.findings.provider; +package com.checkmarx.eclipse.devassist.ui.findings.provider; import java.util.Map; import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider; @@ -107,10 +107,6 @@ protected void paint(Event event, Object element) { Map counts = fileNode.getProblemCount(); if (counts != null && !counts.isEmpty()) { - // CRITICAL FIX: Reset clipping so SWT allows drawing outside the text area - org.eclipse.swt.graphics.Rectangle oldClipping = event.gc.getClipping(); - event.gc.setClipping((org.eclipse.swt.graphics.Rectangle) null); - try { // Determine exactly where the file label ends horizontally Point textSize = event.gc.textExtent(fileNode.getFileName()); @@ -134,28 +130,35 @@ protected void paint(Event event, Object element) { // Draw Count Number tightly next to the shield String countStr = String.valueOf(count); - + // Match text color dynamically (Use foreground selection color if item is highlighted) - // Match text color dynamically (Use foreground selection color if item is highlighted) if ((event.detail & SWT.SELECTED) != 0) { event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); } else { - // Falls back to standard list item text color cleanly across dark/light themes event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_FOREGROUND)); } - + + // Make count text bold + org.eclipse.swt.graphics.Font originalFont = event.gc.getFont(); + org.eclipse.swt.graphics.FontData[] fontData = originalFont.getFontData(); + for (org.eclipse.swt.graphics.FontData fd : fontData) { + fd.setStyle(fd.getStyle() | SWT.BOLD); + } + org.eclipse.swt.graphics.Font boldFont = new org.eclipse.swt.graphics.Font(event.display, fontData); + event.gc.setFont(boldFont); + event.gc.drawString(countStr, currentX, textY, true); + + // Restore original font + event.gc.setFont(originalFont); + boldFont.dispose(); // Advance cursor layout pointer to the next shield group block currentX += event.gc.textExtent(countStr).x + BETWEEN_BADGE_SPACING; } } } - } finally { - // Restore original clipping area - event.gc.setClipping(oldClipping); } } } -} -} +} \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java index 9d395d30..13d1a362 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/CheckmarxDocumentListener.java @@ -24,6 +24,8 @@ public class CheckmarxDocumentListener implements IDocumentListener { 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. @@ -59,6 +61,19 @@ public void documentAboutToBeChanged(DocumentEvent event) { @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) { @@ -73,6 +88,10 @@ public void documentChanged(DocumentEvent event) { } } + public void setSkipNextChange(boolean skip) { + this.skipNextChange = skip; + } + /** * Dispose this listener and clean up associated resources. * Call this when the editor is closed. From eacc7061fcfbbad7e4f6a3fe25cd2e23313ceed9 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Wed, 5 Aug 2026 16:19:27 +0530 Subject: [PATCH 7/9] Total findings number in tab title --- .../devassist/problems/ProblemHolderService.java | 16 +++++++++++++++- .../devassist/ui/findings/CxFindingsView.java | 7 ++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java index c552ab6f..f6d2e627 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -44,7 +44,21 @@ public class ProblemHolderService { * @return the instance of this service for the given project. */ public static ProblemHolderService getInstance(IProject project) { - return ProblemHolderService.getInstance(project); + if (project == null) { + return null; + } + try { + org.eclipse.core.runtime.QualifiedName key = new org.eclipse.core.runtime.QualifiedName( + "com.checkmarx.eclipse.plugin", "problem-holder-service"); + ProblemHolderService instance = (ProblemHolderService) project.getSessionProperty(key); + if (instance == null) { + instance = new ProblemHolderService(); + project.setSessionProperty(key, instance); + } + return instance; + } catch (Exception e) { + return new ProblemHolderService(); + } } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java index f0aefd80..eac31468 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -1433,13 +1433,14 @@ private void refreshTreeWithFilter() { // ✅ Verify treeViewer control before manipulating UI if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { - + treeViewer.setInput(filteredIssues); - + treeViewer.expandAll(); } - + // Update view title with problem count + setPartName("Checkmarx One Assist Findings " + totalAfter); } /** From 53a54e9f62b085d9a3308612efb6d9920dfcf42d Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Wed, 5 Aug 2026 18:39:20 +0530 Subject: [PATCH 8/9] review comments resolved --- checkmarx-ast-eclipse-plugin-tests/pom.xml | 2 +- .../backend/DevAssistScanStateHolder.java | 106 ++++-- .../devassist/backend/DevAssistUtils.java | 179 ----------- .../devassist/backend/ScannerRegistry.java | 36 +-- .../listener/ProjectLifecycleListener.java | 33 +- .../basescanner/BaseScannerCommand.java | 5 +- .../eclipse/devassist/common/ScanManager.java | 20 +- .../listeners/DevAssistProjectListener.java | 303 ------------------ .../eclipse/devassist/model/Location.java | 16 + .../devassist/problems/ProblemDecorator.java | 65 ++-- .../problems/ScanIssueProcessor.java | 6 +- .../scanners/asca/AscaScanResultAdaptor.java | 2 +- .../ContainerScanResultAdaptor.java | 2 +- .../containers/ContainerScannerCommand.java | 16 +- .../containers/ContainerScannerService.java | 2 +- .../scanners/iac/IacScanResultAdaptor.java | 2 +- .../scanners/iac/IacScannerCommand.java | 17 +- .../scanners/iac/IacScannerService.java | 2 +- .../scanners/oss/OssScanResultAdaptor.java | 2 +- .../scanners/oss/OssScannerCommand.java | 42 +-- .../secrets/SecretsScanResultAdaptor.java | 2 +- .../secrets/SecretsScannerCommand.java | 21 +- .../secrets/SecretsScannerService.java | 2 +- .../devassist/ui/findings/CxFindingsView.java | 36 ++- .../editor/FindingsEditorOverlay.java | 4 + .../ui/findings/model/FileNodeLabel.java | 3 + .../ui/findings/realtime/RealTimeScanJob.java | 1 - .../devassist/utils/DevAssistUtils.java | 7 +- 28 files changed, 304 insertions(+), 630 deletions(-) delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java delete mode 100644 checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java diff --git a/checkmarx-ast-eclipse-plugin-tests/pom.xml b/checkmarx-ast-eclipse-plugin-tests/pom.xml index 95ccb716..70e9bd00 100644 --- a/checkmarx-ast-eclipse-plugin-tests/pom.xml +++ b/checkmarx-ast-eclipse-plugin-tests/pom.xml @@ -56,7 +56,7 @@ true false junit5 - false + true ${tycho.testArgLine} ${test.includes} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java index 567fc887..c7e1265f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistScanStateHolder.java @@ -1,6 +1,8 @@ package com.checkmarx.eclipse.devassist.backend; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.security.MessageDigest; import com.checkmarx.eclipse.utils.CxLogger; @@ -22,6 +24,9 @@ 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. @@ -53,11 +58,13 @@ public void updateStateHash(String filePath, long stateHash) { } /** - * Check if a file has changed since last scan. + * 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 (or never scanned), false if unchanged + * @return true if file changed AND no other scan is in-flight, false otherwise */ public boolean hasChanged(String filePath, long currentStateHash) { if (filePath == null) { @@ -69,6 +76,11 @@ public boolean hasChanged(String filePath, long currentStateHash) { // 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; } @@ -76,13 +88,34 @@ public boolean hasChanged(String filePath, long currentStateHash) { 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 changed; + 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 */ @@ -92,25 +125,28 @@ public void clearFileState(String filePath) { } 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 modification timestamp (if open in editor with unsaved changes) + * - Document content hash (if open in editor with unsaved changes) * - * When a file is edited in Eclipse but not saved to disk, the file system - * timestamp doesn't change. This method detects unsaved changes by checking - * if the editor's dirty flag is set, and includes that in the hash. + * 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 @@ -121,8 +157,8 @@ public static long computeFileStateHash(String filePath) { long fileModified = java.nio.file.Files.getLastModifiedTime(path).toMillis(); // Check if file is open in editor with unsaved changes - // If dirty (unsaved), include a dynamic component to detect changes - boolean hasUnsavedChanges = false; + // 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()) { @@ -131,33 +167,39 @@ public static long computeFileStateHash(String filePath) { for (org.eclipse.ui.IEditorReference ref : page.getEditorReferences()) { org.eclipse.ui.IEditorPart editor = ref.getEditor(false); if (editor != null && editor.isDirty()) { - // Check if this editor is for our file try { String editorPath = editor.getEditorInput().getAdapter(org.eclipse.core.resources.IFile.class) .getLocation().toOSString(); if (editorPath.equals(filePath)) { - hasUnsavedChanges = true; - break; + // 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 path + // Skip if we can't get editor or document } } } - if (hasUnsavedChanges) break; + if (dirtyDocumentContent != null) break; } - if (hasUnsavedChanges) break; + if (dirtyDocumentContent != null) break; } } } catch (Exception e) { // If workbench check fails, just use file timestamp - hasUnsavedChanges = false; + dirtyDocumentContent = null; } - // If file has unsaved changes, use current time to force re-scan - // This ensures edits are detected even if not yet saved to disk - if (hasUnsavedChanges) { - return System.nanoTime(); // Force different hash on every check while dirty + // 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; @@ -167,6 +209,30 @@ public static long computeFileStateHash(String filePath) { } } + /** + * 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. * diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java deleted file mode 100644 index 9650f308..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/DevAssistUtils.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.checkmarx.eclipse.devassist.backend; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Base64; -import java.util.List; -import java.util.Objects; - -import org.eclipse.jgit.annotations.NonNull; - -import com.checkmarx.eclipse.utils.CxLogger; - -/** - * Utility class for DevAssist backend operations. Mirrors JetBrains - * DevAssistUtils pattern. - */ -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"; - public static final List CONTAINER_HELM_EXTENSION = List.of("yml", - "yaml"); - 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) { - // Concatenate components with delimiter for clarity - 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). - * Converts uppercase/lowercase/mixed case to proper format. Examples: "MEDIUM" - * → "Medium", "low" → "Low", "Critical" → "Critical" - * - * @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 as-is if not recognized, will be treated as UNKNOWN in icon lookup - return severity; - } - } - - /** - * Check if severity represents a problem (displayable finding). Returns false - * for OK, UNKNOWN, and IGNORED severities. Mirrors JetBrains implementation for - * UI filtering. - * - * @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. Looks for - * "docker-compose" in the filename (case-insensitive). - * - * @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"); - } - - public static boolean isDockerFile(@NonNull String filePath) { - return Paths.get(filePath).getFileName().toString().toLowerCase().contains("dockerfile"); - } - - public static boolean isYamlFile(String filePath) { - if (Objects.isNull(filePath) || filePath.isBlank()) { - return false; - } - String fileExtension = DevAssistUtils.getFileExtension(filePath); - return Objects.nonNull(fileExtension) - && 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); - } - return null; - } -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java index c739b7f4..8d624232 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -41,34 +41,6 @@ public ScannerRegistry(IProject project) { CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); } - /** - * Initialize all available scanners. - * - * Called when project opens. Scanners are created but not yet active; - * activation is controlled by GlobalScannerController. - */ - public void registerAllScanners() { - if (disposed) { - CxLogger.warning(LOG_TAG + " Registry is disposed, cannot register scanners"); - return; - } - - CxLogger.info(LOG_TAG + " Registering all scanners for: " + project.getName()); - - // Scanners will be created lazily via getScannerService() - // For now, just initialize placeholders to track scanner types - ScannerType[] scannerTypes = { - ScannerType.OSS, - ScannerType.SECRETS, - ScannerType.CONTAINERS, - ScannerType.IAC, - ScannerType.ASCA - }; - - for (ScannerType type : scannerTypes) { - CxLogger.info(LOG_TAG + " ✓ Scanner registered: " + type); - } - } /** * Deregister and dispose all scanners (on project close). @@ -82,7 +54,7 @@ public void deregisterAllScanners() { if (scanner instanceof AutoCloseable) { ((AutoCloseable) scanner).close(); } - CxLogger.info(LOG_TAG + " ✓ Disposed scanner: " + type); + CxLogger.info(LOG_TAG + "Disposed scanner: " + type); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing scanner " + type + ": " + e.getMessage()); @@ -147,13 +119,13 @@ private Object createScannerInstance(ScannerType type) { } if (scanner != null) { - CxLogger.info(LOG_TAG + " ✓ Successfully created scanner: " + type.getDisplayName()); + CxLogger.info(LOG_TAG + "Successfully created scanner: " + type.getDisplayName()); } else { - CxLogger.warning(LOG_TAG + " ⚠ Scanner returned null: " + type.getDisplayName()); + 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); + CxLogger.error(LOG_TAG + "Error creating scanner " + type.getDisplayName() + ": " + e.getMessage(), e); e.printStackTrace(); return null; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java index 58c1881d..9049e681 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -31,6 +31,7 @@ public class ProjectLifecycleListener implements IResourceChangeListener { private static final QualifiedName REGISTRY_KEY = new QualifiedName(PLUGIN_ID, "scanner-registry"); private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); private static final QualifiedName STATE_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "state-holder"); + private static final QualifiedName WORKSPACE_SCAN_JOB_KEY = new QualifiedName(PLUGIN_ID, "workspace-scan-job"); private final List initializedProjects = new ArrayList<>(); @@ -133,7 +134,6 @@ private void onProjectOpen(IProject project) { } ScannerRegistry registry = new ScannerRegistry(project); - registry.registerAllScanners(); project.setSessionProperty(REGISTRY_KEY, registry); ProblemHolderService problemHolder = new ProblemHolderService(); @@ -160,6 +160,17 @@ 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) { @@ -213,12 +224,25 @@ protected IStatus run(IProgressMonitor monitor) { try { monitor.beginTask("Scanning manifest, IaC, and container files...", 3); + // Check if job was cancelled or project closed before starting + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + scanManifestFiles(project); monitor.worked(1); + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + scanIacFiles(project); monitor.worked(1); + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + scanContainerFiles(project); monitor.worked(1); @@ -233,6 +257,13 @@ protected IStatus run(IProgressMonitor monitor) { } }; + 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(); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java index 41f775ba..bf747a40 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java @@ -16,6 +16,7 @@ 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. @@ -44,6 +45,7 @@ public void register(IProject project) { } CxLogger.info(config.getEnabledMessage() + ":" + project.getName()); initializeScanner(); + isRegistered = true; } /** @@ -58,6 +60,7 @@ public void deregister(IProject project) { return; } CxLogger.info(config.getDisabledMessage() + ":" + project.getName()); + isRegistered = false; } /** @@ -73,7 +76,7 @@ private boolean getScannerActivationStatus() { * @param project is required */ private boolean isScannerRegisteredAlready(IProject project) { - return project != null && project.isOpen(); + return isRegistered; } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java index 15cd963d..119205a3 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -97,37 +97,35 @@ public List scanFile(String filePath) throws Exception { } // 4. Execute all scanners and merge results - + List allIssues = new ArrayList<>(); int scannerIndex = 1; + int successfulScanners = 0; for (ScannerService scanner : applicableScanners) { String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; try { - - var scanResult = scanner.scan(filePath); List scannerResults = scanResult != null ? scanResult.getIssues() : null; - if (scannerResults == null) { - - } else { - + if (scannerResults != null) { for (ScanIssue issue : scannerResults) { } allIssues.addAll(scannerResults); } + successfulScanners++; } catch (Exception e) { - // Log but continue with other scanners - System.err.println(LOG_TAG + " ✗ ERROR in " + displayName + ": " + e.getMessage()); e.printStackTrace(); } scannerIndex++; } - // 5. Update state hash to mark as scanned - stateHolder.updateStateHash(filePath, currentStateHash); + // 5. Update state hash only if at least one scanner succeeded + // If all scanners failed, don't update hash so file will be re-scanned on next change + if (successfulScanners > 0) { + stateHolder.updateStateHash(filePath, currentStateHash); + } return allIssues; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java deleted file mode 100644 index 967b3e32..00000000 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/listeners/DevAssistProjectListener.java +++ /dev/null @@ -1,303 +0,0 @@ -package com.checkmarx.eclipse.devassist.listeners; - -import java.util.ArrayList; -import java.util.List; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IResourceChangeEvent; -import org.eclipse.core.resources.IResourceChangeListener; -import org.eclipse.core.resources.IResourceDelta; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.QualifiedName; -import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; -import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; -import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; -import com.checkmarx.eclipse.devassist.backend.result.ResultPublisher; -import com.checkmarx.eclipse.devassist.common.ScanManager; -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.utils.CxLogger; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; - -/** - * ProjectListener is responsible for listening for project open/close events and - * managing scanner registration and deregistration for each project. - */ -public class DevAssistProjectListener implements IResourceChangeListener { - - private static final String LOG_TAG = "[PROJECT-LISTENER]"; - private static final String PLUGIN_ID = "com.checkmarx.eclipse.plugin"; - - private static final QualifiedName REGISTRY_KEY = new QualifiedName(PLUGIN_ID, "scanner-registry"); - private static final QualifiedName PROBLEM_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "problem-holder"); - private static final QualifiedName STATE_HOLDER_KEY = new QualifiedName(PLUGIN_ID, "state-holder"); - - private final List initializedProjects = new ArrayList<>(); - - /** - * Register this listener with Eclipse workspace and process existing open projects. - */ - public void register() { - CxLogger.info(LOG_TAG + " Registering project lifecycle listener"); - ResourcesPlugin.getWorkspace().addResourceChangeListener( - this, - IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE - ); - CxLogger.info(LOG_TAG + " ✓ Registered"); - - initExistingProjects(); - } - - /** - * Re-runs initialization for any already-open projects. - */ - public void scanAlreadyOpenProjects() { - initExistingProjects(); - } - - /** - * Scans the workspace and initializes any projects that are already open. - */ - private void initExistingProjects() { - try { - IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); - for (IProject project : projects) { - if (project.isOpen() && !isInitialized(project)) { - - onProjectOpen(project); - } - } - } catch (Exception e) { - CxLogger.error(LOG_TAG + " Error initializing existing projects on startup: " + e.getMessage(), e); - } - } - - public void unregister() { - CxLogger.info(LOG_TAG + " Unregistering project lifecycle listener"); - ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); - } - - /** - * Handle resource change events for project state changes (open/close). - */ - @Override - public void resourceChanged(IResourceChangeEvent event) { - try { - if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { - IResource resource = event.getResource(); - if (resource instanceof IProject) { - onProjectClose((IProject) resource); - } - return; - } - - 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; - if ((delta.getFlags() & IResourceDelta.OPEN) != 0) { - if (project.isOpen() && !isInitialized(project)) { - onProjectOpen(project); - } else if (!project.isOpen() && isInitialized(project)) { - onProjectClose(project); - } - } - } - return true; - }); - } - } catch (Exception e) { - CxLogger.error(LOG_TAG + " Error handling resource change: " + e.getMessage(), e); - } - } - - private void onProjectOpen(IProject project) { - String projName = project.getName(); - if (projName.length() > 26) projName = projName.substring(0, 26); - try { - if (!isUserAuthenticated()) { - return; - } - - ScannerRegistry registry = new ScannerRegistry(project); - registry.registerAllScanners(); - project.setSessionProperty(REGISTRY_KEY, registry); - - ProblemHolderService problemHolder = new ProblemHolderService(); - project.setSessionProperty(PROBLEM_HOLDER_KEY, problemHolder); - DevAssistScanStateHolder stateHolder = new DevAssistScanStateHolder(); - project.setSessionProperty(STATE_HOLDER_KEY, stateHolder); - initializedProjects.add(project.getName()); - - startWorkspaceFileScanning(project); - - } catch (Exception e) { - e.printStackTrace(); - CxLogger.error(LOG_TAG + " Error initializing project " + - project.getName() + ": " + e.getMessage(), e); - } - } - - private boolean isUserAuthenticated() { - String apiKey = com.checkmarx.eclipse.properties.Preferences.getApiKey(); - return apiKey != null && !apiKey.trim().isEmpty(); - } - - private void onProjectClose(IProject project) { - CxLogger.info(LOG_TAG + " ✓ Project closing: " + project.getName()); - - try { - try { - ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty(REGISTRY_KEY); - if (registry != null) { - registry.deregisterAllScanners(); - CxLogger.info(LOG_TAG + " ✓ ScannerRegistry disposed"); - } - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Error disposing ScannerRegistry: " + e.getMessage()); - } - - try { - ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); - if (problemHolder != null) { - problemHolder.clearAll(); - CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); - } - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Error clearing cache: " + e.getMessage()); - } - - try { - DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); - if (stateHolder != null) { - stateHolder.clearAll(); - CxLogger.info(LOG_TAG + " ✓ State holder cleared"); - } - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Error clearing state: " + e.getMessage()); - } - - initializedProjects.remove(project.getName()); - CxLogger.info(LOG_TAG + " ✓ Project cleanup completed: " + project.getName()); - - } catch (Exception e) { - CxLogger.error(LOG_TAG + " Error cleaning up project " + project.getName() + ": " + e.getMessage(), e); - } - } - - private boolean isInitialized(IProject project) { - return initializedProjects.contains(project.getName()); - } - - public String getStatistics() { - return "Initialized projects: " + initializedProjects.size(); - } - - private void startWorkspaceFileScanning(IProject project) { - Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - monitor.beginTask("Scanning manifest, IaC, and container files...", 3); - - scanManifestFiles(project); - monitor.worked(1); - - scanIacFiles(project); - monitor.worked(1); - - scanContainerFiles(project); - monitor.worked(1); - - return Status.OK_STATUS; - - } catch (Exception e) { - e.printStackTrace(); - return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); - } finally { - monitor.done(); - } - } - }; - - scanJob.setPriority(Job.BUILD); - scanJob.schedule(); - } - - private void scanManifestFiles(IProject project) { - String[] manifestPatterns = { - "pom.xml", "package.json", "package-lock.json", "npm-shrinkwrap.json", - "go.mod", "go.sum", "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", - "Gemfile", "Gemfile.lock", "Cargo.toml", "Cargo.lock", "composer.json", "composer.lock", - "packages.config", ".csproj", "yarn.lock" - }; - findAndScanFiles(project, manifestPatterns, "OSS Manifest Files"); - } - - private void scanIacFiles(IProject project) { - String[] iacPatterns = { ".tf", ".tfvars", ".yaml", ".yml", ".hcl" }; - findAndScanFiles(project, iacPatterns, "IaC Configuration Files"); - } - - private void scanContainerFiles(IProject project) { - String[] containerPatterns = { - "Dockerfile", "dockerfile", "docker-compose.yaml", "docker-compose.yml", ".dockerignore" - }; - findAndScanFiles(project, containerPatterns, "Container Files"); - } - - private void findAndScanFiles(IProject project, String[] patterns, String fileType) { - try { - - - ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( - new QualifiedName(PLUGIN_ID, "scanner-registry")); - DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( - new QualifiedName(PLUGIN_ID, "state-holder")); - ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( - new QualifiedName(PLUGIN_ID, "problem-holder")); - - if (registry == null || stateHolder == null || problemHolder == null) { - return; - } - - IResource[] members = project.members(true); - for (IResource resource : members) { - if (!(resource instanceof org.eclipse.core.resources.IFile)) { - continue; - } - - IFile file = (org.eclipse.core.resources.IFile) resource; - String fileName = file.getName().toLowerCase(); - String filePath = file.getLocation().toOSString(); - - boolean matches = false; - for (String pattern : patterns) { - if (fileName.equals(pattern.toLowerCase()) || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { - matches = true; - break; - } - } - if (matches) { - try { - ScanManager scanManager = new ScanManager(registry, stateHolder); - List issues = scanManager.scanFile(filePath); - if (!issues.isEmpty()) { - problemHolder.addScanIssues(filePath, issues); - ResultPublisher.publishResults(file, issues); - } - } catch (Exception e) { - System.err.println(LOG_TAG + " Error scanning " + fileName + ": " + e.getMessage()); - } - } - } - } catch (Exception e) { - System.err.println(LOG_TAG + " Error finding files for " + fileType + ": " + e.getMessage()); - } - } -} diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java index f6fc6cfa..7e010a9d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/model/Location.java @@ -9,6 +9,7 @@ public class Location { private int line; private int startIndex; private int endIndex; + private boolean isAbsoluteOffset = false; public Location() { } @@ -19,6 +20,13 @@ public Location(int line, int startIndex, int endIndex) { 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; } @@ -42,4 +50,12 @@ public int getEndIndex() { public void setEndIndex(int endIndex) { this.endIndex = endIndex; } + + public boolean isAbsoluteOffset() { + return isAbsoluteOffset; + } + + public void setAbsoluteOffset(boolean absoluteOffset) { + isAbsoluteOffset = absoluteOffset; + } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java index 293c3693..75293f5a 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -8,7 +8,6 @@ import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.source.Annotation; -import org.eclipse.jface.text.source.AnnotationModel; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; @@ -53,44 +52,39 @@ public static void decorateEditor(IFile file, List scanIssues) { return; } if (scanIssues == null) { - return; - } - if (scanIssues.isEmpty()) { - return; + scanIssues = List.of(); } // **FIX: Use getLocation() (absolute path) for consistency with RealTimeScanJob and ResultPublisher** // This ensures fileAnnotations map keys match the same path format used throughout the codebase String filePath = file.getLocation().toOSString(); - try { // Find open editor for this file - ITextEditor editor = findOpenEditor(file); if (editor == null) { - - CxLogger.info(LOG_TAG + " ✗ No open editor for: " + filePath); + CxLogger.info(LOG_TAG + "No open editor for: " + filePath); return; } - // Get annotation model from editor - IAnnotationModel annotationModel = editor.getDocumentProvider() .getAnnotationModel(editor.getEditorInput()); if (annotationModel == null) { - - CxLogger.warning(LOG_TAG + " ✗ No annotation model available"); + CxLogger.warning(LOG_TAG + "No annotation model available"); return; } - - // Remove previous annotations for this file - + // Remove previous annotations for this file (BEFORE isEmpty check) + // This ensures stale annotations are cleared even if file is now clean clearAnnotations(filePath, annotationModel); + // Early return if no issues to add + if (scanIssues.isEmpty()) { + return; + } + // Add new annotations for each issue List annotations = new java.util.ArrayList<>(); @@ -127,9 +121,9 @@ public static void decorateEditor(IFile file, List scanIssues) { // Add annotation to model for display annotationModel.addAnnotation(annotation, pos); - CxLogger.info(LOG_TAG + " ✓ Annotation added to model"); + CxLogger.info(LOG_TAG + "Annotation added to model"); } else { - CxLogger.warning(LOG_TAG + " ✗ FAILED: Invalid position (offset=" + + CxLogger.warning(LOG_TAG + "FAILED: Invalid position (offset=" + (pos != null ? pos.getOffset() : "null") + ", length=" + (pos != null ? pos.getLength() : "null") + ")"); } @@ -145,7 +139,7 @@ public static void decorateEditor(IFile file, List scanIssues) { fileAnnotations.put(filePath, annotations); CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); - CxLogger.info(LOG_TAG + " ✓ COMPLETE: Added " + annotations.size() + + CxLogger.info(LOG_TAG + "COMPLETE: Added " + annotations.size() + " annotations to editor"); CxLogger.info(LOG_TAG + " ══════════════════════════════════════════════════"); @@ -312,7 +306,7 @@ private static org.eclipse.jface.text.Position decorateOssFirstLineOnly( return null; } - CxLogger.info(LOG_TAG + " [OSS] ✓ Decorating first line: [" + lineOffset + + CxLogger.info(LOG_TAG + " [OSS] Decorating first line: [" + lineOffset + "-" + (lineOffset + decorationLength) + "] = " + decorationLength + " chars"); return new org.eclipse.jface.text.Position(lineOffset, decorationLength); @@ -355,10 +349,11 @@ private static org.eclipse.jface.text.Position calculateRange(ITextEditor editor int lineLength = lineInfo.getLength(); int trimIndent = getLeadingWhitespaceOffset(document, lineOffset, lineLength); - boolean isLineRelative = (rawStart == 0 && line > 0) || (rawEnd < 100 && rawEnd - rawStart < 100); + // Use explicit flag from Location instead of inferring from magnitude + boolean isAbsoluteOffset = location.isAbsoluteOffset(); - int charStart = isLineRelative ? (lineOffset + rawStart) : rawStart; - int charEnd = isLineRelative ? (lineOffset + rawEnd) : rawEnd; + 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) { @@ -443,7 +438,7 @@ private static void clearAnnotations(String filePath, } fileAnnotations.remove(filePath); - CxLogger.info(LOG_TAG + " ✓ Cleared " + previousAnnotations.size() + + CxLogger.info(LOG_TAG + " Cleared " + previousAnnotations.size() + " previous annotations"); } } catch (Exception e) { @@ -538,7 +533,7 @@ public static void clearDecorations(IFile file) { clearAnnotations(filePath, annotationModel); } - CxLogger.info(LOG_TAG + " ✓ Decorations cleared"); + CxLogger.info(LOG_TAG + " Decorations cleared"); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing decorations: " + @@ -562,10 +557,10 @@ public static String getStatistics() { /** * Highlight a line and add gutter icon for a problem. * - * Called by ScanIssueProcessor during per-issue processing. - * Integrates with the existing decoration system. + * 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 (currently unused, for JetBrains API alignment) + * @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 @@ -576,9 +571,19 @@ public void highlightLineAddGutterIconForProblem( boolean isProblem, int problemLineNumber) { + if (!isProblem || scanIssue == null) { + return; + } + try { - CxLogger.info(LOG_TAG + " highlightLineAddGutterIconForProblem called for line: " + - problemLineNumber + " issue: " + scanIssue.getTitle()); + // 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); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java index 9581ff06..7d7a9088 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/problems/ScanIssueProcessor.java @@ -173,12 +173,14 @@ private ProblemDescriptor processValidIssue( /** * 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 + * @return true if problem, false if info/note/unknown/ok/ignored */ private boolean isProblem(String severity) { - return severity.equals("critical") || + return severity.equals("malicious") || + severity.equals("critical") || severity.equals("high") || severity.equals("medium") || severity.equals("low"); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java index 5d6a49b6..f98b2b3a 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java @@ -6,7 +6,7 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.utils.CxLogger; import java.util.*; import java.util.stream.Collectors; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java index f085373d..6c95c32f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScanResultAdaptor.java @@ -9,7 +9,7 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.utils.CxLogger; import java.util.Collections; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java index 7a8cf70c..81f67eb1 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerCommand.java @@ -1,6 +1,7 @@ package com.checkmarx.eclipse.devassist.scanners.containers; import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; @@ -11,12 +12,12 @@ /** * 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 { +public class ContainerScannerCommand extends BaseScannerCommand { private static final String LOG_TAG = "[CONTAINER-COMMAND]"; - private final IProject project; private final ContainerScannerService containerScannerService; private boolean isInitialized = false; @@ -36,15 +37,16 @@ public ContainerScannerCommand(IProject project) { * @param containerScannerService custom or pre-configured scanner service */ public ContainerScannerCommand(IProject project, ContainerScannerService containerScannerService) { - this.project = project; + super(project, ContainerScannerService.createConfig()); this.containerScannerService = containerScannerService; - initializeScanner(); + CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); } /** - * Initializes the scanner, invoked during or after registration of the command. + * Initializes the scanner, invoked when scanner is registered. */ - public synchronized void initializeScanner() { + @Override + public void initializeScanner() { if (!isInitialized) { this.isInitialized = true; String projectName = Objects.nonNull(project) ? project.getName() : "Unknown"; @@ -90,6 +92,7 @@ public ContainerScannerService getScannerService() { * 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) { @@ -101,5 +104,6 @@ public void dispose() { } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing Container Scanner Command: " + e.getMessage()); } + super.dispose(); } } \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index 2be3e68d..f55305f7 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -2,7 +2,7 @@ import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.common.ScanResult; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java index f8d42b37..05baecfb 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -7,7 +7,7 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.utils.CxLogger; import java.util.*; import java.util.stream.Collectors; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java index 8861df0a..0644bd10 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerCommand.java @@ -1,6 +1,7 @@ package com.checkmarx.eclipse.devassist.scanners.iac; import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; @@ -11,19 +12,18 @@ * * 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 { +public class IacScannerCommand extends BaseScannerCommand { private static final String LOG_TAG = "[IAC-COMMAND]"; - private final IProject project; private final IacScannerService scannerService; public IacScannerCommand(IProject project, IacScannerService scannerService) { - this.project = project; + super(project, IacScannerService.createConfig()); this.scannerService = scannerService; CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); - initializeScanner(); } public IacScannerCommand(IProject project) { @@ -31,11 +31,12 @@ public IacScannerCommand(IProject project) { } /** - * Initializes the scanner, invoked after creation / registration of the scanner. + * 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() { - // Intentionally empty - mirrors JetBrains implementation where IaC scans - // are triggered on demand via editor file changes rather than bulk project scans. + CxLogger.info(LOG_TAG + " Initialized for project: " + project.getName()); } /** @@ -63,6 +64,7 @@ public ScanResult scan(String filePath, IDocument document) * Disposes the scanner and releases associated resources. * Triggered when project is closed or scanner is unregistered. */ + @Override public void dispose() { try { scannerService.close(); @@ -70,5 +72,6 @@ public void dispose() { } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); } + super.dispose(); } } \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java index 8be0d6e3..17731b0b 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -8,7 +8,7 @@ import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.apache.commons.lang3.tuple.Pair; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java index 1037573e..17df8fab 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScanResultAdaptor.java @@ -9,7 +9,7 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.utils.CxLogger; import java.util.Collections; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java index 786eb33d..b9fb39c1 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -1,11 +1,13 @@ package com.checkmarx.eclipse.devassist.scanners.oss; -import com.checkmarx.ast.ossrealtime.OssRealtimeResults; -import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.utils.CxLogger; +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; @@ -18,41 +20,41 @@ import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; -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 com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.utils.CxLogger; /** * Command for coordinating OSS scanner operations in Eclipse. * * Manages the lifecycle and initialization of OSS scanning: + * - Extends BaseScannerCommand for consistent registration lifecycle * - Traverses project workspace files recursively upon initialization * - Executes background job scans on supported manifest files * - Publishes findings via ProblemHolderService */ -public class OssScannerCommand { +public class OssScannerCommand extends BaseScannerCommand { private static final String LOG_TAG = "[OSS-COMMAND]"; public final OssScannerService ossScannerService; - private final IProject project; public OssScannerCommand(IProject project) { + super(project, OssScannerService.createConfig()); this.ossScannerService = new OssScannerService(project); - this.project = project; CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); - initializeScanner(); } /** - * Initializes the scanner, invoked after creation. + * Initializes the scanner, invoked when scanner is registered. * Launches a background Eclipse Job to scan all manifest files in the project workspace. */ - protected void initializeScanner() { + @Override + public void initializeScanner() { Job scanJob = new Job("Starting Checkmarx OSS Real-time Scan") { @Override protected IStatus run(IProgressMonitor monitor) { @@ -163,6 +165,7 @@ public ScanResult scan(String filePath) { /** * Disposes the scanner and releases resources. */ + @Override public void dispose() { try { ossScannerService.close(); @@ -170,5 +173,6 @@ public void dispose() { } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); } + super.dispose(); } } \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java index 586058a3..5f908056 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScanResultAdaptor.java @@ -7,7 +7,7 @@ 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.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import java.util.Collections; import java.util.List; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java index bfb0f238..b5bb60bd 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerCommand.java @@ -1,6 +1,7 @@ package com.checkmarx.eclipse.devassist.scanners.secrets; import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; @@ -8,19 +9,29 @@ /** * Command for coordinating Secrets scanner operations. + * Extends BaseScannerCommand for consistent registration lifecycle. */ -public class SecretsScannerCommand { +public class SecretsScannerCommand extends BaseScannerCommand { - private final IProject project; - private final SecretsScannerService scannerService; private static final String LOG_TAG = "[SECRETS-COMMAND]"; + private final SecretsScannerService scannerService; + public SecretsScannerCommand(IProject project) { - this.project = 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); } @@ -29,6 +40,7 @@ public ScanResult scan(String filePath, IDocument docume return scannerService.scan(filePath, document, project); } + @Override public void dispose() { try { scannerService.close(); @@ -36,5 +48,6 @@ public void dispose() { } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing: " + e.getMessage()); } + super.dispose(); } } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java index e2b98dce..6671978d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -8,7 +8,7 @@ import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.backend.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.utils.CxLogger; import org.eclipse.core.resources.IProject; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java index eac31468..185351f7 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -1397,7 +1397,7 @@ private void refreshTreeWithFilter() { String issueId = issue.getScanIssueId(); boolean isIgnored = ignoredStore != null && ignoredStore.isIgnored(issueId); boolean hasFilter = filterState.hasFilter(issue.getSeverity()); - boolean isProblem = com.checkmarx.eclipse.devassist.backend.DevAssistUtils.isProblem(issue.getSeverity()); + boolean isProblem = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.isProblem(issue.getSeverity()); @@ -1434,9 +1434,39 @@ private void refreshTreeWithFilter() { // ✅ Verify treeViewer control before manipulating UI if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { - treeViewer.setInput(filteredIssues); + // Save current expansion state to avoid full tree rebuild + Object[] expandedElements = treeViewer.getExpandedElements(); - treeViewer.expandAll(); + // Use setInput() for initial population, refresh() for subsequent updates + Object currentInput = treeViewer.getInput(); + if (currentInput == null) { + // First time: full tree setup with initial data + treeViewer.setInput(filteredIssues); + treeViewer.expandAll(); + } else { + // Subsequent updates: use targeted refresh instead of full rebuild + // This avoids rebuilding the entire tree on every single-file scan + treeViewer.setInput(filteredIssues); + + // Restore expansion state for files that still exist in filtered results + java.util.List validExpanded = new java.util.ArrayList<>(); + for (Object element : expandedElements) { + if (element instanceof com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel) { + com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel fileNode = + (com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel) element; + if (filteredIssues.containsKey(fileNode.getFilePath())) { + validExpanded.add(element); + } + } + } + + if (!validExpanded.isEmpty()) { + treeViewer.setExpandedElements(validExpanded.toArray()); + } else { + // If no previous expansion state, expand all + treeViewer.expandAll(); + } + } } // Update view title with problem count diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java index f3bd7843..b1bed62e 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java @@ -22,6 +22,7 @@ public class FindingsEditorOverlay { // These match the annotation types defined in plugin.xml + private static final String ANNOTATION_TYPE_MALICIOUS = "com.checkmarx.eclipse.findings.malicious"; private static final String ANNOTATION_TYPE_CRITICAL = "com.checkmarx.eclipse.findings.critical"; private static final String ANNOTATION_TYPE_HIGH = "com.checkmarx.eclipse.findings.high"; private static final String ANNOTATION_TYPE_MEDIUM = "com.checkmarx.eclipse.findings.medium"; @@ -113,6 +114,7 @@ public static void clearHighlights(TextEditor editor) { /** * Get annotation type based on severity level. + * Maps all problem severities to their corresponding annotation types. */ private static String getAnnotationTypeForSeverity(String severity) { if (severity == null) { @@ -120,6 +122,8 @@ private static String getAnnotationTypeForSeverity(String severity) { } switch (severity.toLowerCase()) { + case "malicious": + return ANNOTATION_TYPE_MALICIOUS; case "critical": case "high": return ANNOTATION_TYPE_CRITICAL; diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java index 8baf4e55..3c8cbaba 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/model/FileNodeLabel.java @@ -1,6 +1,7 @@ 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; @@ -37,6 +38,7 @@ public FileNodeLabel(String fileName, String filePath, List issues, M /** * Calculate problem counts grouped by severity. + * Severity keys are normalized to lowercase for consistent lookups. */ private static Map calculateProblemCount(List issues) { Map counts = new HashMap<>(); @@ -48,6 +50,7 @@ private static Map calculateProblemCount(List issues) { 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); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java index 65f4e262..64e95904 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/ui/findings/realtime/RealTimeScanJob.java @@ -145,7 +145,6 @@ protected IStatus run(IProgressMonitor monitor) { if (registry == null) { registry = new com.checkmarx.eclipse.devassist.backend.ScannerRegistry(project); - registry.registerAllScanners(); project.setSessionProperty(registryKey, registry); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java index 66a0b7c6..4ff57485 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -18,6 +18,10 @@ public class DevAssistUtils { private static final String LOG_TAG = "[DEV-ASSIST-UTILS]"; + public static final String DOCKERFILE = "dockerfile"; + public static final String DOCKER_COMPOSE = "docker-compose"; + public static final String HELM = "helm"; + private DevAssistUtils() { // Private constructor to prevent instantiation } @@ -85,7 +89,6 @@ public static String normalizeSeverity(String severity) { if (severity == null || severity.isEmpty()) { return "Unknown"; } - String upper = severity.toUpperCase(); switch (upper) { case "MALICIOUS": @@ -156,7 +159,7 @@ public static boolean isYamlFile(String filePath) { } String fileExtension = getFileExtension(filePath); return Objects.nonNull(fileExtension) - && DevAssistConstants.CONTAINER_HELM_EXTENSION.contains(fileExtension.toLowerCase()); + && DevAssistConstants.CONTAINER_HELM_EXTENSION.contains(fileExtension.toLowerCase()); } /** From f4012a157da62054cf25cecd24d9ec6645e88cae Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Wed, 5 Aug 2026 23:30:32 +0530 Subject: [PATCH 9/9] Remove automatic scan triggers on file navigation and tab switching Issue: Scans were being triggered immediately when files were opened or when users switched between tabs, causing unnecessary scan overhead. Root cause: - setupRealtimeScanning() called scanJob.reschedule(0) on file open - partActivated() called scanJob.reschedule(0) on tab switch Solution: Remove these automatic triggers. Scans are now ONLY triggered by: - Actual user edits via documentChanged() in CheckmarxDocumentListener - The isInitialLoad flag ensures first document load doesn't trigger scan Behavior: - Open file: No scan (cached decorations applied only) - Switch tabs: No scan (applies cached decorations only) - Edit file: Scan triggered after 1s debounce (actual user typing) Co-Authored-By: Claude Haiku 4.5 --- .../backend/result/ResultPublisher.java | 41 ++++++++- .../eclipse/devassist/common/ScanManager.java | 86 ++++++++++--------- .../scanners/asca/AscaScannerService.java | 4 +- .../containers/ContainerScannerService.java | 3 +- .../scanners/iac/IacScannerService.java | 3 +- .../scanners/oss/OssScannerService.java | 3 +- .../secrets/SecretsScannerService.java | 3 +- .../devassist/utils/DevAssistUtils.java | 75 ++++++++++++++++ 8 files changed, 172 insertions(+), 46 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java index 17257772..ae812f57 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -163,7 +163,7 @@ private static void createAndRenderDecorations(IFile file, List scanI ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( new QualifiedName("com.checkmarx.eclipse.plugin", "scanner-registry")); DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( - new QualifiedName("com.checkmarx.eclipse.plugin", "scan-state-holder")); + new QualifiedName("com.checkmarx.eclipse.plugin", "state-holder")); ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); @@ -177,8 +177,10 @@ private static void createAndRenderDecorations(IFile file, List scanI // 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()); @@ -205,6 +207,43 @@ private static void createAndRenderDecorations(IFile file, List scanI } } + /** + * Get the IDocument for a file, preferring the live editor's document (so unsaved + * edits are reflected) and falling back to reading the file's on-disk content. + * + * ScanIssueProcessor requires a non-null document to validate that an issue's line + * number is within range (getNumberOfLines()); without it every issue is rejected. + * + * @param file File to get the document for + * @return IDocument, or null if it could not be obtained + */ + private static org.eclipse.jface.text.IDocument getDocumentForFile(IFile file) { + try { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page != null) { + org.eclipse.ui.IEditorPart editor = page.findEditor(new org.eclipse.ui.part.FileEditorInput(file)); + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + org.eclipse.jface.text.IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + if (doc != null) { + return doc; + } + } + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Could not get document from editor: " + e.getMessage()); + } + + try { + org.eclipse.jface.text.Document doc = new org.eclipse.jface.text.Document(); + doc.set(new String(file.getContents().readAllBytes())); + return doc; + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Could not create document from file: " + e.getMessage()); + return null; + } + } + /** * Find the open Findings View. * diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java index 119205a3..8c01134f 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -72,62 +72,68 @@ public List scanFile(String filePath) throws Exception { // 2. Check if file changed since last scan - + // NOTE: hasChanged() atomically marks the file as "in-flight" when it returns true. + // We MUST call stateHolder.markScanComplete(filePath) once we're done (success or + // failure) or every subsequent edit will be permanently BLOCKED as "already in-flight". if (!stateHolder.hasChanged(filePath, currentStateHash)) { - + return List.of(); } - - // 3. Get all scanners that support this file - - List> applicableScanners = factory.getAllSupportedScanners(filePath); + try { + // 3. Get all scanners that support this file - - for (ScannerService scanner : applicableScanners) { - String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; - - } + List> applicableScanners = factory.getAllSupportedScanners(filePath); - if (applicableScanners.isEmpty()) { - - // Still update state to avoid re-checking unsupported files - stateHolder.updateStateHash(filePath, currentStateHash); - return List.of(); - } - // 4. Execute all scanners and merge results + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; + + } + + if (applicableScanners.isEmpty()) { + + // Still update state to avoid re-checking unsupported files + stateHolder.updateStateHash(filePath, currentStateHash); + return List.of(); + } - List allIssues = new ArrayList<>(); - int scannerIndex = 1; - int successfulScanners = 0; + // 4. Execute all scanners and merge results - for (ScannerService scanner : applicableScanners) { - String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; - try { - var scanResult = scanner.scan(filePath); - List scannerResults = scanResult != null ? scanResult.getIssues() : null; + List allIssues = new ArrayList<>(); + int scannerIndex = 1; + int successfulScanners = 0; - if (scannerResults != null) { - for (ScanIssue issue : scannerResults) { + for (ScannerService scanner : applicableScanners) { + String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; + try { + var scanResult = scanner.scan(filePath); + List scannerResults = scanResult != null ? scanResult.getIssues() : null; + + if (scannerResults != null) { + for (ScanIssue issue : scannerResults) { + } + allIssues.addAll(scannerResults); } - allIssues.addAll(scannerResults); + successfulScanners++; + + } catch (Exception e) { + e.printStackTrace(); } - successfulScanners++; + scannerIndex++; + } - } catch (Exception e) { - e.printStackTrace(); + // 5. Update state hash only if at least one scanner succeeded + // If all scanners failed, don't update hash so file will be re-scanned on next change + if (successfulScanners > 0) { + stateHolder.updateStateHash(filePath, currentStateHash); } - scannerIndex++; - } - // 5. Update state hash only if at least one scanner succeeded - // If all scanners failed, don't update hash so file will be re-scanned on next change - if (successfulScanners > 0) { - stateHolder.updateStateHash(filePath, currentStateHash); + return allIssues; + } finally { + // Always release the in-flight marker so the next edit can trigger a scan. + stateHolder.markScanComplete(filePath); } - - return allIssues; } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java index 683ec706..d3f951a2 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -68,7 +68,9 @@ protected boolean isFileTypeSupported(String filePath) { @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { - com.checkmarx.eclipse.devassist.common.ScanResult result = scanWithDocument(filePath, new Document()); + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + com.checkmarx.eclipse.devassist.common.ScanResult result = scanWithDocument(filePath, + liveDocument != null ? liveDocument : new Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (com.checkmarx.eclipse.devassist.common.ScanResult) result; } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index f55305f7..6359ec88 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -285,7 +285,8 @@ public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { return null; } - return scan(filePath, null, project); + IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument, project); } @Override diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java index 17731b0b..93934a2d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -180,7 +180,8 @@ public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { return null; } - return scan(filePath, new Document(), project); + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument != null ? liveDocument : new Document(), project); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java index 981540a9..0727a45d 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -78,7 +78,8 @@ public void close() throws Exception { @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { - return scanWithDocument(filePath, new Document()); + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scanWithDocument(filePath, liveDocument != null ? liveDocument : new Document()); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java index 6671978d..5563f010 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -163,7 +163,8 @@ public ScanResult scan(String filePath) { if (!shouldScanFile(filePath)) { return null; } - return scan(filePath, new Document(), project); + IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument != null ? liveDocument : new Document(), project); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java index 4ff57485..8e04fc14 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -6,7 +6,17 @@ import java.util.List; import java.util.Objects; +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; import com.checkmarx.eclipse.devassist.backend.SeverityLevel; import com.checkmarx.eclipse.utils.CxLogger; @@ -180,4 +190,69 @@ public static String getFileExtension(String filePath) { } 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]; + } }