diff --git a/.gitignore b/.gitignore index 17efd6c3..976783a6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ *.jar !checkmarx-ast-eclipse-plugin/lib/*.jar !devassist-lib/lib/*.jar -!common-lib/lib/*.jar \ No newline at end of file +!common-lib/lib/*.jar diff --git a/checkmarx-ast-eclipse-plugin/icons/severity/star-action.svg b/checkmarx-ast-eclipse-plugin/icons/severity/star-action.svg new file mode 100644 index 00000000..bfc23248 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin/icons/severity/star-action.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java index 263a7fa2..a119f9cd 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 @@ -51,7 +51,8 @@ public static String convertStringTimeStamp(String timestamp) { Instant instant = Instant.parse(timestamp); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN).withZone(ZoneId.systemDefault()); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PARAM_TIMESTAMP_PATTERN) + .withZone(ZoneId.systemDefault()); parsedDate = dateTimeFormatter.format(instant); } catch (Exception e) { CxLogger.warning(String.format("[TIME-STAMP] Returning time stamp", e.getMessage())); @@ -99,19 +100,21 @@ public static void setTextForComboViewer(ComboViewer comboViewer, String text) { */ public static void updateFiltersEnabledAndCheckedState(List filterActions) { for (Action action : filterActions) { - // avoid to disable group by severity , group by query name and group by state actions - if (!action.getId().equals(ActionName.GROUP_BY_SEVERITY.name()) && !action.getId().equals(ActionName.GROUP_BY_QUERY_NAME.name()) && !action.getId().equals(ActionName.GROUP_BY_STATE_NAME.name()) ) { + // avoid to disable group by severity , group by query name and group by state + // actions + if (!action.getId().equals(ActionName.GROUP_BY_SEVERITY.name()) + && !action.getId().equals(ActionName.GROUP_BY_QUERY_NAME.name()) + && !action.getId().equals(ActionName.GROUP_BY_STATE_NAME.name())) { action.setEnabled(DataProvider.getInstance().containsResults()); } - - if(!action.getId().equals(ActionName.FILTER_CHANGED.name())) { + + if (!action.getId().equals(ActionName.FILTER_CHANGED.name())) { action.setChecked(FilterState.isSeverityEnabled(action.getId())); } - } } - + /** * Create a display model to be presented in the tree * @@ -121,7 +124,7 @@ public static void updateFiltersEnabledAndCheckedState(List filterAction public static DisplayModel message(String message) { return new DisplayModel.DisplayModelBuilder(message).build(); } - + /** * Show message in the tree * @@ -132,17 +135,16 @@ public static void showMessage(DisplayModel rootModel, TreeViewer viewer, String rootModel.children.add(PluginUtils.message(message)); viewer.refresh(); } - - + /** * Clear message in the tree * */ - public static void clearMessage(DisplayModel rootModel,TreeViewer viewer) { + public static void clearMessage(DisplayModel rootModel, TreeViewer viewer) { rootModel.children.clear(); viewer.refresh(); } - + /** * Get Event Broker * @@ -151,7 +153,7 @@ public static void clearMessage(DisplayModel rootModel,TreeViewer viewer) { public static IEventBroker getEventBroker() { return (IEventBroker) PlatformUI.getWorkbench().getService(IEventBroker.class); } - + /** * Check if checkmarx credentials are defined in the Preferences * @@ -160,7 +162,7 @@ public static IEventBroker getEventBroker() { public static boolean areCredentialsDefined() { return StringUtils.isNotBlank(Preferences.getApiKey()); } - + /** * Add Checkmarx vulnerabilities to Problems View * @@ -194,7 +196,7 @@ public static void addVulnerabilitiesToProblemsView(List resultsList) { } } } - + /** * Get IMarker severity based on each checkmarx result severity * @@ -203,25 +205,25 @@ public static void addVulnerabilitiesToProblemsView(List resultsList) { */ private static Integer getIMarkerSeverity(String resultSeverity) { Severity severity = Severity.getSeverity(resultSeverity); - + switch (severity) { - case CRITICAL: - return IMarker.SEVERITY_ERROR; - case HIGH: - return IMarker.SEVERITY_ERROR; - case MEDIUM: - return IMarker.SEVERITY_WARNING; - case LOW: - return IMarker.SEVERITY_INFO; - case INFO: - return IMarker.SEVERITY_INFO; - default: - break; + case CRITICAL: + return IMarker.SEVERITY_ERROR; + case HIGH: + return IMarker.SEVERITY_ERROR; + case MEDIUM: + return IMarker.SEVERITY_WARNING; + case LOW: + return IMarker.SEVERITY_INFO; + case INFO: + return IMarker.SEVERITY_INFO; + default: + break; } - + return IMarker.SEVERITY_INFO; } - + /** * Find files in workspace * @@ -231,7 +233,8 @@ private static Integer getIMarkerSeverity(String resultSeverity) { public static List findFileInWorkspace(final String fileName) { final List foundFiles = new ArrayList(); try { - // visiting only resources proxy because we obtain the resource only when matching name, thus the workspace traversal is much faster + // visiting only resources proxy because we obtain the resource only when + // matching name, thus the workspace traversal is much faster ResourcesPlugin.getWorkspace().getRoot().accept(new IResourceProxyVisitor() { @Override public boolean visit(IResourceProxy resourceProxy) throws CoreException { @@ -250,7 +253,7 @@ public boolean visit(IResourceProxy resourceProxy) throws CoreException { } return foundFiles; } - + /** * Clear checkmarx vulnerabilities from Problems View */ @@ -258,15 +261,16 @@ public static void clearVulnerabilitiesFromProblemsView() { IWorkspace workspace = ResourcesPlugin.getWorkspace(); IResource resource = workspace.getRoot(); IMarker[] markers; - + try { markers = resource.findMarkers(IMarker.MARKER, true, IResource.DEPTH_INFINITE); - + for (IMarker m : markers) { - if(m.getAttribute(IMarker.SOURCE_ID) != null && m.getAttribute(IMarker.SOURCE_ID).equals(PluginConstants.PROBLEM_SOURCE_ID)) { + if (m.getAttribute(IMarker.SOURCE_ID) != null + && m.getAttribute(IMarker.SOURCE_ID).equals(PluginConstants.PROBLEM_SOURCE_ID)) { m.delete(); } - } + } } catch (CoreException e) { CxLogger.error(String.format(PluginConstants.ERROR_FINDING_OR_DELETING_MARKER, e.getMessage()), e); } diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java index 78534c90..d29e4b41 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java @@ -91,11 +91,16 @@ import com.checkmarx.ast.wrapper.CxException; import com.checkmarx.eclipse.common.events.SettingsTopics; import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.events.SettingsTopics; +import com.checkmarx.eclipse.common.preferences.Preferences; import com.checkmarx.eclipse.Activator; import com.checkmarx.eclipse.enums.ActionName; import com.checkmarx.eclipse.common.enums.Severity; import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.enums.Severity; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; import com.checkmarx.eclipse.utils.NotificationPopUpUI; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.actions.ToolBarActions; @@ -122,12 +127,12 @@ public class CheckmarxView extends ViewPart implements EventHandler { private static final String FORMATTED_SCAN_LABEL_LATEST = "%s %s (%s)"; private boolean isUpdatingCombo = false; private boolean resetStoredProjects = false; - + private Timer debounceTimer = new Timer("ProjectSearchDebounce", true); private TimerTask pendingSearchTask; private static final int DEBOUNCE_DELAY_MS = 400; private volatile String latestProjectSearchTerm = ""; - + private static final int SCROLL_WIDTH = 30; /** * The ID of the view as specified by the extension. @@ -160,7 +165,8 @@ public class CheckmarxView extends ViewPart implements EventHandler { public static final Image BFL = Activator.getImageDescriptor("/icons/checkmarx-plugin-13_dark.png").createImage(); private TreeViewer resultsTree; - private ComboViewer scanIdComboViewer, projectComboViewer, branchComboViewer, triageSeverityComboViewew, triageStateComboViewer; + private ComboViewer scanIdComboViewer, projectComboViewer, branchComboViewer, triageSeverityComboViewew, + triageStateComboViewer; private ISelectionChangedListener triageSeverityComboViewerListener, triageStateComboViewerListener; private Text commentText; private DisplayModel rootModel; @@ -186,8 +192,8 @@ public class CheckmarxView extends ViewPart implements EventHandler { private Link codeBashingLinkText; private CLabel attackVectorLabel; - //private CLabel bflLabel; - //private Text bflText; + // private CLabel bflLabel; + // private Text bflText; private Label attackVectorSeparator; private ToolBarActions toolBarActions; @@ -204,7 +210,6 @@ public class CheckmarxView extends ViewPart implements EventHandler { private List currentProjects = new ArrayList<>(); private List storeCurrentProjects = new ArrayList<>(); - private boolean scansCleanedByProject = false; private boolean firstTimeTriggered = false; @@ -250,9 +255,12 @@ public void dispose() { public void createPartControl(Composite parent) { this.parent = parent; - // Clear any stale vulnerability markers from a previous session before drawing the view. - // Markers persist as real Eclipse IMarkers across restarts, so without this cleanup, - // vulnerabilities from prior scans can appear until the user changes project/branch/scan. + // Clear any stale vulnerability markers from a previous session before drawing + // the view. + // Markers persist as real Eclipse IMarkers across restarts, so without this + // cleanup, + // vulnerabilities from prior scans can appear until the user changes + // project/branch/scan. PluginUtils.clearVulnerabilitiesFromProblemsView(); if (PluginUtils.areCredentialsDefined()) { @@ -278,11 +286,11 @@ public void onRefsChanged(RefsChangedEvent arg) { } String gitBranch = arg.getRepository().getBranch(); - - if(gitBranch.equals(currentBranch)) { + + if (gitBranch.equals(currentBranch)) { return; } - + updatePluginBranchAndScans(gitBranch); } catch (IOException e) { CxLogger.error(PluginConstants.ERROR_GETTING_GIT_BRANCH, e); @@ -322,7 +330,8 @@ private void createToolbar() { pluginEventBus.register(this); toolBarActions = new ToolBarActions.ToolBarActionsBuilder().actionBars(actionBars).rootModel(rootModel) - .resultsTree(resultsTree).pluginEventBus(pluginEventBus).projectsCombo(projectComboViewer).branchesCombo(branchComboViewer).scansCombo(scanIdComboViewer).build(); + .resultsTree(resultsTree).pluginEventBus(pluginEventBus).projectsCombo(projectComboViewer) + .branchesCombo(branchComboViewer).scansCombo(scanIdComboViewer).build(); } @Override @@ -361,7 +370,7 @@ private void drawPluginPanel() { // Create plugin toolBar createToolbar(); - + loadComboboxes(); // Init git branch listener @@ -409,7 +418,8 @@ protected IStatus run(IProgressMonitor arg0) { if (currentProjectId.isEmpty() || currentProjects.isEmpty()) { PluginUtils.setTextForComboViewer(projectComboViewer, PROJECT_COMBO_VIEWER_TEXT); PluginUtils.setTextForComboViewer(branchComboViewer, BRANCH_COMBO_VIEWER_TEXT); - PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); + PluginUtils.setTextForComboViewer(scanIdComboViewer, + PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); PluginUtils.enableComboViewer(projectComboViewer, true); PluginUtils.enableComboViewer(scanIdComboViewer, true); PluginUtils.enableComboViewer(branchComboViewer, false); @@ -430,7 +440,7 @@ protected IStatus run(IProgressMonitor arg0) { PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_PLACEHOLDER); }); - + if (!currentBranch.isEmpty()) { updateStartScanButton(true); sync.asyncExec(() -> { @@ -446,7 +456,7 @@ protected IStatus run(IProgressMonitor arg0) { }); if (!currentScanId.isEmpty()) { - String currentScanName = getScanNameFromId(scanList, currentScanId); + String currentScanName = getScanNameFromId(scanList, currentScanId); currentScanIdFormmated = currentScanName; sync.asyncExec(() -> { PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanName); @@ -703,7 +713,7 @@ public void handleEvent(Event e) { * * @param resultsComposite */ - private void createResultVulnerabilitiesPanel(Composite resultsComposite) { + private void createResultVulnerabilitiesPanel(Composite resultsComposite) { attackVectorCompositePanel = new Composite(resultsComposite, SWT.BORDER); attackVectorCompositePanel.setLayout(new FillLayout()); attackVectorCompositePanel.setVisible(false); @@ -721,48 +731,54 @@ private void drawAttackVectorSeparator(Composite parent) { * draw BFL composite */ - /*private void drawBFLComposite() { - bflComposite = new Composite(attackVectorContentComposite, SWT.NONE); - GridLayout bflCompositeLayout = new GridLayout(2, false); - bflComposite.setLayout(bflCompositeLayout); - bflComposite.setSize(attackVectorContentComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - GridData gd_blfComposite = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1); - bflComposite.setLayoutData(gd_blfComposite); - - bflComposite.setBackground(attackVectorContentComposite.getBackground()); - - bflLabel = new CLabel(bflComposite, SWT.HORIZONTAL); - GridData gd_emptyLabel = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1); - gd_emptyLabel.heightHint = PluginConstants.BFL_LABEL_HEIGHT; - bflLabel.setLayoutData(gd_emptyLabel); - bflLabel.setBackground(bflComposite.getBackground()); - - bflText = new Text(bflComposite, SWT.WRAP | SWT.MULTI); - GridData gd_bflText = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); - gd_bflText.widthHint = PluginConstants.BFL_TEXT_MAX_WIDTH; - bflText.setLayoutData(gd_bflText); - bflText.setBackground(bflComposite.getBackground()); - bflText.setData(PluginConstants.DATA_ID_KEY, PluginConstants.BEST_FIX_LOCATION); - }*/ + /* + * private void drawBFLComposite() { + * bflComposite = new Composite(attackVectorContentComposite, SWT.NONE); + * GridLayout bflCompositeLayout = new GridLayout(2, false); + * bflComposite.setLayout(bflCompositeLayout); + * bflComposite.setSize(attackVectorContentComposite.computeSize(SWT.DEFAULT, + * SWT.DEFAULT)); + * GridData gd_blfComposite = new GridData(SWT.FILL, SWT.FILL, false, false, 1, + * 1); + * bflComposite.setLayoutData(gd_blfComposite); + * + * bflComposite.setBackground(attackVectorContentComposite.getBackground()); + * + * bflLabel = new CLabel(bflComposite, SWT.HORIZONTAL); + * GridData gd_emptyLabel = new GridData(SWT.FILL, SWT.FILL, false, false, 1, + * 1); + * gd_emptyLabel.heightHint = PluginConstants.BFL_LABEL_HEIGHT; + * bflLabel.setLayoutData(gd_emptyLabel); + * bflLabel.setBackground(bflComposite.getBackground()); + * + * bflText = new Text(bflComposite, SWT.WRAP | SWT.MULTI); + * GridData gd_bflText = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); + * gd_bflText.widthHint = PluginConstants.BFL_TEXT_MAX_WIDTH; + * bflText.setLayoutData(gd_bflText); + * bflText.setBackground(bflComposite.getBackground()); + * bflText.setData(PluginConstants.DATA_ID_KEY, + * PluginConstants.BEST_FIX_LOCATION); + * } + */ /** * Draw panel when Checkmarx credentials are not defined */ private void drawMissingCredentialsPanel() { - // Dispose all children to remove any previous panels (plugin panel, etc.) - for (Control child : parent.getChildren()) { - child.dispose(); - } + // Dispose all children to remove any previous panels (plugin panel, etc.) + for (Control child : parent.getChildren()) { + child.dispose(); + } - // Set parent layout for credentials panel - GridLayout parentLayout = new GridLayout(1, true); - parent.setLayout(parentLayout); + // Set parent layout for credentials panel + GridLayout parentLayout = new GridLayout(1, true); + parent.setLayout(parentLayout); openSettingsComposite = new Composite(parent, SWT.NONE); openSettingsComposite.setLayout(new GridLayout(1, true)); // This is the key line: center horizontally and vertically, and expand to fill - openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + openSettingsComposite.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); final Label hidden = new Label(openSettingsComposite, SWT.NONE); hidden.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); @@ -810,8 +826,7 @@ public String getText(Object element) { return super.getText(element); } }); - - + projectComboViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { @@ -843,7 +858,8 @@ protected IStatus run(IProgressMonitor arg0) { currentBranches = DataProvider.getInstance().getBranchesForProject(selectedProject.getId()); sync.asyncExec(() -> { branchComboViewer.setInput(currentBranches); - PluginUtils.setTextForComboViewer(branchComboViewer, currentBranches.isEmpty() ? NO_BRANCHES_AVAILABLE : BRANCH_COMBO_VIEWER_TEXT); + PluginUtils.setTextForComboViewer(branchComboViewer, + currentBranches.isEmpty() ? NO_BRANCHES_AVAILABLE : BRANCH_COMBO_VIEWER_TEXT); PluginUtils.enableComboViewer(branchComboViewer, true); PluginUtils.enableComboViewer(scanIdComboViewer, true); PluginUtils.updateFiltersEnabledAndCheckedState(toolBarActions.getFilterActions()); @@ -855,12 +871,12 @@ protected IStatus run(IProgressMonitor arg0) { } }; job.schedule(); - //After project selected and branches loaded reset the project list - if(resetStoredProjects) { + // After project selected and branches loaded reset the project list + if (resetStoredProjects) { storeCurrentProjects.add(selectedProject); preservCaretposition(storeCurrentProjects, selectedProject.getName()); currentProjects = storeCurrentProjects; - resetStoredProjects=false; + resetStoredProjects = false; } } } @@ -868,25 +884,28 @@ protected IStatus run(IProgressMonitor arg0) { // Add ModifyListener to handle manual text input for projects projectComboViewer.getCombo().addModifyListener(e -> { - if (isUpdatingCombo) return; + if (isUpdatingCombo) + return; String enteredProject = projectComboViewer.getCombo().getText().trim(); - + // Skip search if the text is the default instruction if (enteredProject.equals(PROJECT_COMBO_VIEWER_TEXT) || enteredProject.equals(LOADING_PROJECTS)) { updateStartScanButton(false); // Disable scan button return; } - // If user starts typing again and list is empty, restore currentProjects - if (projectComboViewer.getCombo().getItemCount() == 0 && !currentProjects.isEmpty() && enteredProject.length()>0) { - isUpdatingCombo = true; - preservCaretposition(currentProjects,enteredProject); - isUpdatingCombo = false; - } - + // If user starts typing again and list is empty, restore currentProjects + if (projectComboViewer.getCombo().getItemCount() == 0 && !currentProjects.isEmpty() + && enteredProject.length() > 0) { + isUpdatingCombo = true; + preservCaretposition(currentProjects, enteredProject); + isUpdatingCombo = false; + } + latestProjectSearchTerm = enteredProject; // Track the latest term List matchedProjects; matchedProjects = currentProjects.stream().map(Project::getName) - .filter(name -> name != null && name.toLowerCase().contains(enteredProject.toLowerCase())).limit(100) + .filter(name -> name != null && name.toLowerCase().contains(enteredProject.toLowerCase())) + .limit(100) .collect(Collectors.toList()); if (matchedProjects.isEmpty()) { CxLogger.info("Entered project is not exist in current projects list"); @@ -911,11 +930,11 @@ protected IStatus run(IProgressMonitor monitor) { isUpdatingCombo = true; // Update UI in UI thread if (searchedProjects != null && !searchedProjects.isEmpty()) { - preservCaretposition(searchedProjects,searchTerm); + preservCaretposition(searchedProjects, searchTerm); currentProjects = searchedProjects; - resetStoredProjects=true; + resetStoredProjects = true; } else { - preservCaretposition(Collections.emptyList(),searchTerm); + preservCaretposition(Collections.emptyList(), searchTerm); updateStartScanButton(false); // Disable scan button isUpdatingCombo = false; return; @@ -925,7 +944,7 @@ protected IStatus run(IProgressMonitor monitor) { }); } catch (Exception ex) { ex.printStackTrace(); - } + } return Status.OK_STATUS; } }; @@ -933,17 +952,19 @@ protected IStatus run(IProgressMonitor monitor) { } }; debounceTimer.schedule(pendingSearchTask, DEBOUNCE_DELAY_MS); - + } }); - // Add FocusListener to disable branch combo when project is cleared and focus lost + // Add FocusListener to disable branch combo when project is cleared and focus + // lost projectComboViewer.getCombo().addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { // When user clicks outside project combo, check if project is empty String enteredProject = projectComboViewer.getCombo().getText().trim(); - // If project field is empty or contains only the placeholder text, disable branch combo + // If project field is empty or contains only the placeholder text, disable + // branch combo if (enteredProject.isEmpty() || enteredProject.equals(PROJECT_COMBO_VIEWER_TEXT)) { currentProjectId = PluginConstants.EMPTY_STRING; PluginUtils.enableComboViewer(branchComboViewer, false); @@ -957,6 +978,7 @@ public void focusGained(FocusEvent e) { }); } + /** * Update state variables and make plugin fields loading when project changes * @@ -1079,7 +1101,7 @@ public void run() { } private void loadLatestScanByDefault(List scanList) { - if(scanList.isEmpty()) { + if (scanList.isEmpty()) { PluginUtils.setTextForComboViewer(scanIdComboViewer, PluginConstants.COMBOBOX_SCAND_ID_NO_SCANS_AVAILABLE); return; } else { @@ -1090,12 +1112,13 @@ private void loadLatestScanByDefault(List scanList) { currentScanIdFormmated = getScanNameFromId(scanList, currentScanId); scanIdComboViewer.setSelection(new StructuredSelection(currentScanId)); PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanIdFormmated); - PluginUtils.showMessage(rootModel, resultsTree, String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, latestScanId)); - alreadyRunning=true; - updateResultsTree(currentScanId,false); + PluginUtils.showMessage(rootModel, resultsTree, + String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, latestScanId)); + alreadyRunning = true; + updateResultsTree(currentScanId, false); GlobalSettings.storeInPreferences(GlobalSettings.PARAM_SCAN_ID, currentScanId); }); - + } /** @@ -1146,7 +1169,7 @@ public void handleEvent(Event event) { public String getText(Object element) { if (element instanceof Scan) { // Always fetch the latest scan id from preferences before rendering - if(!GlobalSettings.getFromPreferences("LATEST_SCAN_ID", "").isEmpty()) { + if (!GlobalSettings.getFromPreferences("LATEST_SCAN_ID", "").isEmpty()) { latestScanId = GlobalSettings.getFromPreferences("LATEST_SCAN_ID", ""); } Scan scan = (Scan) element; @@ -1174,7 +1197,8 @@ protected IStatus run(IProgressMonitor arg0) { } if (selection.size() > 0) { sync.asyncExec(() -> { - PluginUtils.showMessage(rootModel, resultsTree, String.format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, selectedScan.getId())); + PluginUtils.showMessage(rootModel, resultsTree, String + .format(PluginConstants.RETRIEVING_RESULTS_FOR_SCAN, selectedScan.getId())); PluginUtils.enableComboViewer(projectComboViewer, false); PluginUtils.enableComboViewer(branchComboViewer, false); }); @@ -1230,22 +1254,22 @@ private String getScanNameFromId(List scans, String scanId) { private String formatScanLabel(Scan scan) { String formattedString = ""; String updatedAtDate = PluginUtils.convertStringTimeStamp(scan.getUpdatedAt()); - if(!latestScanId.isEmpty() && scan.getId().equalsIgnoreCase(latestScanId)) { - formattedString = String.format(FORMATTED_SCAN_LABEL_LATEST, updatedAtDate ,scan.getId() ,"latest"); + if (!latestScanId.isEmpty() && scan.getId().equalsIgnoreCase(latestScanId)) { + formattedString = String.format(FORMATTED_SCAN_LABEL_LATEST, updatedAtDate, scan.getId(), "latest"); } else { - - formattedString = String.format(FORMATTED_SCAN_LABEL, updatedAtDate, scan.getId()); + + formattedString = String.format(FORMATTED_SCAN_LABEL, updatedAtDate, scan.getId()); } return formattedString; - + } - + /** * Retrieve latest scan from scanList */ - + private Scan getLatestScanFromScanList(List scanList) { - + return scanList.get(0); } @@ -1254,30 +1278,30 @@ private Scan getLatestScanFromScanList(List scanList) { * on the chosen scan id */ private void setSelectionForProjectComboViewer() { - - if(scanIdComboViewer.getCombo().getText().isEmpty()) { + + if (scanIdComboViewer.getCombo().getText().isEmpty()) { PluginUtils.clearMessage(rootModel, resultsTree); PluginUtils.showMessage(rootModel, resultsTree, PluginConstants.NO_SCAN_ID_PROVIDED); CxLogger.info(String.format(PluginConstants.NO_SCAN_ID_PROVIDED, PluginConstants.EMPTY_STRING)); return; } - + String scanIdText = scanIdComboViewer.getCombo().getText().trim(); String[] parts = scanIdText.split("\\s+"); if (parts.length >= 3) { - scanIdText = parts[2]; + scanIdText = parts[2]; } - + final String scanId = scanIdText; if (currentScanId.equals(scanId)) { - PluginUtils.clearMessage(rootModel, resultsTree); - // reload cached results - List results = DataProvider.getInstance().sortResults(); + PluginUtils.clearMessage(rootModel, resultsTree); + // reload cached results + List results = DataProvider.getInstance().sortResults(); - rootModel.setChildren(results); - resultsTree.refresh(); + rootModel.setChildren(results); + resultsTree.refresh(); PluginUtils.setTextForComboViewer(scanIdComboViewer, currentScanIdFormmated); CxLogger.info(String.format(PluginConstants.INFO_RESULTS_ALREADY_RETRIEVED, scanId)); return; @@ -1314,15 +1338,18 @@ protected IStatus run(IProgressMonitor arg0) { if (projectList.isEmpty()) return null; - // Fetch the project directly by ID — the full list may not contain it (e.g. pagination limits) + // Fetch the project directly by ID — the full list may not contain it (e.g. + // pagination limits) Project fetchedProject = DataProvider.getInstance().getProjectById(projectId); - // Determine project name: prefer the directly-fetched result, fall back to list lookup + // Determine project name: prefer the directly-fetched result, fall back to list + // lookup String projectName = (fetchedProject != null) ? fetchedProject.getName() : getProjectFromId(projectList, projectId); - // If the project was not already in the list, prepend it so it's visible in the dropdown + // If the project was not already in the list, prepend it so it's visible in the + // dropdown if (fetchedProject != null && projectList.stream().noneMatch(p -> p.getId().equals(projectId))) { projectList = new ArrayList<>(projectList); projectList.add(0, fetchedProject); @@ -1451,7 +1478,8 @@ public void selectionChanged(SelectionChangedEvent event) { @Override protected IStatus run(IProgressMonitor arg0) { - if (selectedItem.getResult() != null && selectedItem.getResult().getSimilarityId() != null) { + if (selectedItem.getResult() != null + && selectedItem.getResult().getSimilarityId() != null) { sync.asyncExec(() -> { currentlyDisplayedItem = selectedItem; createTriageSeverityAndStateCombos(selectedItem); @@ -1488,10 +1516,9 @@ private void createTriageSeverityAndStateCombos(DisplayModel selectedItem) { selectedSeverity = selectedItem.getSeverity(); String[] severity = { "CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO" }; - triageSeverityComboViewew.setContentProvider(ArrayContentProvider.getInstance()); - triageSeverityComboViewew.setInput(severity); - PluginUtils.setTextForComboViewer(triageSeverityComboViewew, currentSeverity); - + triageSeverityComboViewew.setContentProvider(ArrayContentProvider.getInstance()); + triageSeverityComboViewew.setInput(severity); + PluginUtils.setTextForComboViewer(triageSeverityComboViewew, currentSeverity); if (triageSeverityComboViewerListener != null) { triageSeverityComboViewew.removeSelectionChangedListener(triageSeverityComboViewerListener); @@ -1510,14 +1537,13 @@ public void selectionChanged(SelectionChangedEvent event) { String currentState = selectedItem.getState(); selectedState = selectedItem.getResult().getState(); - + // [AST-92100] Fetch dynamic states from DataProvider List state = DataProvider.getInstance().getStatesForEngine(selectedItem.getType()); - - triageStateComboViewer.setContentProvider(ArrayContentProvider.getInstance()); - triageStateComboViewer.setInput(state); - PluginUtils.setTextForComboViewer(triageStateComboViewer, currentState); - + + triageStateComboViewer.setContentProvider(ArrayContentProvider.getInstance()); + triageStateComboViewer.setInput(state); + PluginUtils.setTextForComboViewer(triageStateComboViewer, currentState); if (triageStateComboViewerListener != null) { triageStateComboViewer.removeSelectionChangedListener(triageStateComboViewerListener); @@ -1534,7 +1560,7 @@ public void selectionChanged(SelectionChangedEvent event) { triageStateComboViewer.addSelectionChangedListener(triageStateComboViewerListener); if (triageButtonAdapter != null) { triageButton.removeSelectionListener(triageButtonAdapter); - + } triageButtonAdapter = new SelectionAdapter() { @Override @@ -1552,14 +1578,17 @@ public void widgetSelected(SelectionEvent event) { Job job = new Job("Checkmarx: Updating triage information...") { String comment = commentText.getText() != null - && !commentText.getText().equalsIgnoreCase("Notes (Optional or required based on tenant configuration)") ? commentText.getText() - : ""; + && !commentText.getText() + .equalsIgnoreCase("Notes (Optional or required based on tenant configuration)") + ? commentText.getText() + : ""; @Override protected IStatus run(IProgressMonitor arg0) { try { - DataProvider.getInstance().triageUpdate(projectId,similarityId, engineType, selectedState, comment, selectedSeverity); - + DataProvider.getInstance().triageUpdate(projectId, similarityId, engineType, + selectedState, comment, selectedSeverity); + sync.asyncExec(() -> { selectedItem.setSeverity(selectedSeverity); selectedItem.setState(selectedState); @@ -1577,7 +1606,9 @@ protected IStatus run(IProgressMonitor arg0) { }); } catch (Exception e) { sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), "Triage failed", e.getMessage(), null, null, null).open(); + new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow() + .getShell().getDisplay(), "Triage failed", e.getMessage(), null, null, null) + .open(); }); } @@ -1603,10 +1634,9 @@ protected IStatus run(IProgressMonitor arg0) { } } }; - + triageButton.addSelectionListener(triageButtonAdapter); - - + boolean isSCAVulnerability = selectedItem.getType().equalsIgnoreCase(PluginConstants.SCA_DEPENDENCY); triageButton.setVisible(!isSCAVulnerability); triageButton.setEnabled(!isSCAVulnerability); @@ -1667,26 +1697,32 @@ protected IStatus run(IProgressMonitor arg0) { openLink(codeBashing.getPath()); } catch (CxException e) { CxLogger.info(String.format(PluginConstants.CODEBASHING, e.getMessage())); - + if (e.getExitCode() == PluginConstants.EXIT_CODE_LICENSE_NOT_FOUND) { SelectionAdapter onClickCodebashingLink = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { try { - PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(event.text)); + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser() + .openURL(new URL(event.text)); } catch (PartInitException | MalformedURLException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_CODEBASHING_DETAILS, e.getMessage()), e); + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_CODEBASHING_DETAILS, + e.getMessage()), e); } } }; - + sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), PluginConstants.CODEBASHING, + new NotificationPopUpUI( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), + PluginConstants.CODEBASHING, PluginConstants.CODEBASHING_NO_LICENSE, onClickCodebashingLink, null, null).open(); }); } else if (e.getExitCode() == PluginConstants.EXIT_CODE_LESSON_NOT_FOUND) { sync.asyncExec(() -> { - new NotificationPopUpUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), PluginConstants.CODEBASHING, + new NotificationPopUpUI( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().getDisplay(), + PluginConstants.CODEBASHING, PluginConstants.CODEBASHING_NO_LESSON, null, null, null).open(); }); } @@ -1740,7 +1776,9 @@ public void run() { Text descriptionTxt = new Text(detailsComposite, SWT.READ_ONLY | SWT.WRAP | SWT.MULTI); descriptionTxt.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); - descriptionTxt.setText(selectedItem.getResult().getDescription() != null ? selectedItem.getResult().getDescription(): "No data"); + descriptionTxt.setText( + selectedItem.getResult().getDescription() != null ? selectedItem.getResult().getDescription() + : "No data"); descriptionScrolledComposite.setContent(detailsComposite); descriptionScrolledComposite.setMinSize(descriptionScrolledComposite.getSize().x, @@ -1763,7 +1801,8 @@ public void run() { tbtmChanges.setControl(changesScrolledComposite); changesScrolledComposite.setContent(changesComposite); - changesScrolledComposite.setMinSize(changesScrolledComposite.getSize().x, changesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y); + changesScrolledComposite.setMinSize(changesScrolledComposite.getSize().x, + changesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y); scrolledComposite.setContent(tabFolder); @@ -1784,7 +1823,8 @@ protected void populateLoadingScreen() { sync.asyncExec(() -> { Composite loadingScreen = new Composite(scrolledComposite, SWT.NONE); loadingScreen.setLayout(new GridLayout(1, false)); - loadingScreen.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, false, false)); + loadingScreen + .setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, false, false)); CLabel loadingLabel = new CLabel(loadingScreen, SWT.NONE); loadingLabel.setText(PluginConstants.LOADING_CHANGES); @@ -1970,7 +2010,7 @@ private void updateAttackVectorForSelectedTreeItem(DisplayModel selectedItem) { if (selectedItem.getType().equalsIgnoreCase(PluginConstants.SAST)) { drawAttackVector(selectedItem); } - + layoutAttackVectorItemComposite(); }); } @@ -1985,18 +2025,18 @@ private void drawPackageData(DisplayModel selectedItem) { Composite child = new Composite(sc, SWT.NONE); child.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - child.setLayout(new GridLayout(1, false)); + child.setLayout(new GridLayout(1, false)); child.setBackground(attackVectorCompositePanel.getBackground()); drawAttackVectorTitle(child, PluginConstants.PACKAGE_DATA); drawIndividualPackageData(child, selectedItem.getResult().getData().getPackageData()); - + sc.setContent(child); sc.setMinSize(child.computeSize(SWT.DEFAULT, SWT.DEFAULT)); sc.setExpandHorizontal(true); sc.setExpandVertical(true); } - + /** * Draw attack vector title * @@ -2009,9 +2049,9 @@ private void drawAttackVectorTitle(Composite parent, String title) { attackVectorLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1)); attackVectorLabel.setBackground(attackVectorCompositePanel.getBackground()); attackVectorLabel.setText(title); - + attackVectorLabel.layout(); - + drawAttackVectorSeparator(parent); } @@ -2043,30 +2083,32 @@ private void drawAttackVector(DisplayModel selectedItem) { final TabItem remediationExamplesTab = new TabItem(tabFolder, SWT.NONE); remediationExamplesTab.setText(PluginConstants.REMEDIATION_EXAMPLES); - + drawSASTAttackVector(selectedItem, tabFolder, attackVectorTab); - + learnMoreData = null; - + tabFolder.addSelectionListener(new SelectionListener() { @Override - public void widgetDefaultSelected(SelectionEvent arg0) {} + public void widgetDefaultSelected(SelectionEvent arg0) { + } @Override public void widgetSelected(SelectionEvent event) { String tab = event.item != null ? ((TabItem) event.item).getText() : StringUtils.EMPTY; - + switch (tab) { - case PluginConstants.LEARN_MORE: + case PluginConstants.LEARN_MORE: drawSASTLearnMore(selectedItem, tabFolder, learnMoreTab); case PluginConstants.REMEDIATION_EXAMPLES: drawSASTRemediationExamples(selectedItem, tabFolder, remediationExamplesTab); - default: return; + default: + return; } } }); } - + /** * Draw SAST Attack Vector tab * @@ -2075,25 +2117,26 @@ public void widgetSelected(SelectionEvent event) { * @param attackVectorTab */ private void drawSASTAttackVector(DisplayModel selectedItem, TabFolder folder, TabItem attackVectorTab) { - final ScrolledComposite attackVectorScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL | SWT.H_SCROLL); + final ScrolledComposite attackVectorScrolledComposite = new ScrolledComposite(folder, + SWT.V_SCROLL | SWT.H_SCROLL); attackVectorScrolledComposite.setExpandVertical(true); attackVectorScrolledComposite.setExpandHorizontal(true); attackVectorTab.setControl(attackVectorScrolledComposite); final Composite attackVectorComposite = new Composite(attackVectorScrolledComposite, SWT.NONE); attackVectorComposite.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - attackVectorComposite.setLayout(new GridLayout(1, false)); + attackVectorComposite.setLayout(new GridLayout(1, false)); attackVectorScrolledComposite.setContent(attackVectorComposite); - + String queryName = selectedItem.getResult().getData().getQueryName(); String groupName = selectedItem.getResult().getData().getGroup(); List nodesList = selectedItem.getResult().getData().getNodes(); drawIndividualAttackVectorData(attackVectorComposite, queryName, groupName, nodesList, false); - + attackVectorScrolledComposite.setMinSize(attackVectorComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); } - + /** * Draw SAST Learn More tab * @@ -2101,39 +2144,44 @@ private void drawSASTAttackVector(DisplayModel selectedItem, TabFolder folder, T * @param folder * @param learnMoreTab */ - private void drawSASTLearnMore(DisplayModel selectedItem, TabFolder folder, TabItem learnMoreTab) { + private void drawSASTLearnMore(DisplayModel selectedItem, TabFolder folder, TabItem learnMoreTab) { final ScrolledComposite learnMoreScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL); learnMoreScrolledComposite.setExpandHorizontal(true); learnMoreScrolledComposite.setExpandVertical(true); - + final Composite learnMoreComposite = new Composite(learnMoreScrolledComposite, SWT.NONE); learnMoreComposite.setLayout(new GridLayout()); - + learnMoreScrolledComposite.setContent(learnMoreComposite); learnMoreScrolledComposite.setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - - if(learnMoreData == null) { + + if (learnMoreData == null) { CLabel loadingLabel = new CLabel(learnMoreComposite, SWT.NONE); loadingLabel.setText(PluginConstants.LEARN_MORE_LOADING); } - + learnMoreTab.setControl(learnMoreScrolledComposite); - + Job job = new Job(PluginConstants.GETTING_LEARN_MORE_JOB) { @Override protected IStatus run(IProgressMonitor arg0) { sync.asyncExec(() -> { try { - - List learnMoreData = getLearnMoreData(selectedItem.getResult().getData().getQueryId()); - + + List learnMoreData = getLearnMoreData( + selectedItem.getResult().getData().getQueryId()); + clearLearnMoreComposite(learnMoreComposite); - - for(LearnMore learnMore : learnMoreData) { - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_RISK, learnMore.getRisk().trim()); - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_CAUSE, learnMore.getCause().trim()); - addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_GENERAL_RECOMMENDATIONS, learnMore.getGeneralRecommendations().trim()); - + + for (LearnMore learnMore : learnMoreData) { + addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_RISK, + learnMore.getRisk().trim()); + addLearnMoreSectionsToComposite(learnMoreComposite, PluginConstants.LEARN_MORE_CAUSE, + learnMore.getCause().trim()); + addLearnMoreSectionsToComposite(learnMoreComposite, + PluginConstants.LEARN_MORE_GENERAL_RECOMMENDATIONS, + learnMore.getGeneralRecommendations().trim()); + // Adding CWE link in Learn More section of SAST vulnerability String cweId = selectedItem.getResult().getVulnerabilityDetails().getCweId(); if (cweId != null && !cweId.isEmpty()) { @@ -2150,27 +2198,28 @@ protected IStatus run(IProgressMonitor arg0) { }); } - learnMoreScrolledComposite.setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - learnMoreComposite.layout(); + learnMoreScrolledComposite + .setMinSize(learnMoreComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + learnMoreComposite.layout(); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_LEARN_MORE, e.getMessage()), e); - + clearLearnMoreComposite(learnMoreComposite); - + Label learnMoreErrorLabel = new Label(learnMoreComposite, SWT.NONE); learnMoreErrorLabel.setText(e.getMessage()); learnMoreScrolledComposite.setContent(learnMoreComposite); } }); - + return Status.OK_STATUS; } }; job.schedule(); } - + /** * Get and cache learn more data * @@ -2178,16 +2227,16 @@ protected IStatus run(IProgressMonitor arg0) { * @return * @throws Exception */ - private static List getLearnMoreData(String queryId) throws Exception{ - if(learnMoreData != null) { + private static List getLearnMoreData(String queryId) throws Exception { + if (learnMoreData != null) { return learnMoreData; } - + learnMoreData = DataProvider.getInstance().learnMore(queryId); - + return learnMoreData; } - + /** * Draw SAST Remediation Examples tab * @@ -2195,95 +2244,104 @@ private static List getLearnMoreData(String queryId) throws Exception * @param folder * @param remediationExamplesTab */ - private void drawSASTRemediationExamples(DisplayModel selectedItem, TabFolder folder, TabItem remediationExamplesTab) { - final ScrolledComposite remediationExamplesScrolledComposite = new ScrolledComposite(folder, SWT.V_SCROLL | SWT.BORDER); + private void drawSASTRemediationExamples(DisplayModel selectedItem, TabFolder folder, + TabItem remediationExamplesTab) { + final ScrolledComposite remediationExamplesScrolledComposite = new ScrolledComposite(folder, + SWT.V_SCROLL | SWT.BORDER); remediationExamplesScrolledComposite.setExpandHorizontal(true); remediationExamplesScrolledComposite.setExpandVertical(true); - + final Composite remediationExamplesComposite = new Composite(remediationExamplesScrolledComposite, SWT.NONE); remediationExamplesComposite.setLayout(new GridLayout()); - + remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); - remediationExamplesScrolledComposite.setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); - - if(learnMoreData == null) { + remediationExamplesScrolledComposite + .setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + + if (learnMoreData == null) { Label loadingLabel = new Label(remediationExamplesComposite, SWT.NONE); loadingLabel.setText(PluginConstants.LEARN_MORE_LOADING); } - + remediationExamplesTab.setControl(remediationExamplesScrolledComposite); - + Job job = new Job(PluginConstants.GETTING_LEARN_MORE_JOB) { @Override protected IStatus run(IProgressMonitor arg0) { sync.asyncExec(() -> { try { - - List learnMoreData = getLearnMoreData(selectedItem.getResult().getData().getQueryId()); - + + List learnMoreData = getLearnMoreData( + selectedItem.getResult().getData().getQueryId()); + clearLearnMoreComposite(remediationExamplesComposite); - - for(LearnMore learnMore : learnMoreData) { + + for (LearnMore learnMore : learnMoreData) { List samples = learnMore.getSamples(); - - if(samples.size() == 0 ) { + + if (samples.size() == 0) { Label noRemediationLabel = new Label(remediationExamplesComposite, SWT.NONE); noRemediationLabel.setText(PluginConstants.NO_REMEDIATION_EXAMPLES); remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); - + continue; } - - for(Sample sample : samples) { + + for (Sample sample : samples) { StyledText sampleTitle = new StyledText(remediationExamplesComposite, SWT.WRAP); - sampleTitle.setText(String.format(PluginConstants.REMEDIATION_EXAMPLE_TITLE_FORMAT, sample.getTitle(), sample.getProgLanguage())); - GridData titleLayoutData = new GridData( GridData.FILL_HORIZONTAL ) ; + sampleTitle.setText(String.format(PluginConstants.REMEDIATION_EXAMPLE_TITLE_FORMAT, + sample.getTitle(), sample.getProgLanguage())); + GridData titleLayoutData = new GridData(GridData.FILL_HORIZONTAL); titleLayoutData.grabExcessHorizontalSpace = true; titleLayoutData.horizontalAlignment = SWT.FILL; - titleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width - SCROLL_WIDTH; + titleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width + - SCROLL_WIDTH; titleLayoutData.horizontalSpan = 2; sampleTitle.setLayoutData(titleLayoutData); sampleTitle.setMargins(2, 5, 2, 5); - - Composite sampleExampleComposite = new Composite(remediationExamplesComposite, SWT.NONE); + + Composite sampleExampleComposite = new Composite(remediationExamplesComposite, + SWT.NONE); sampleExampleComposite.setBackground(remediationExamplesComposite.getBackground()); GridLayout layout = new GridLayout(); layout.marginHeight = 10; layout.marginWidth = 10; sampleExampleComposite.setLayout(layout); sampleExampleComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - + Label sampleExample = new Label(sampleExampleComposite, SWT.WRAP); - sampleExample.setText(sample.getCode()); - GridData exampleLayoutData = new GridData(GridData.FILL_HORIZONTAL) ; + sampleExample.setText(sample.getCode()); + GridData exampleLayoutData = new GridData(GridData.FILL_HORIZONTAL); exampleLayoutData.grabExcessHorizontalSpace = true; exampleLayoutData.horizontalAlignment = SWT.FILL; - exampleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width - SCROLL_WIDTH; + exampleLayoutData.widthHint = remediationExamplesScrolledComposite.getClientArea().width + - SCROLL_WIDTH; exampleLayoutData.horizontalSpan = 2; sampleExample.setLayoutData(exampleLayoutData); - - remediationExamplesScrolledComposite.setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + + remediationExamplesScrolledComposite + .setMinSize(remediationExamplesComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); remediationExamplesComposite.layout(); } } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_LEARN_MORE, e.getMessage()), e); - + clearLearnMoreComposite(remediationExamplesComposite); - + Label remediationErrorLabel = new Label(remediationExamplesComposite, SWT.NONE); remediationErrorLabel.setText(e.getMessage()); remediationExamplesScrolledComposite.setContent(remediationExamplesComposite); } }); - + return Status.OK_STATUS; } }; job.schedule(); } - + /** * Clear Learn More composite * @@ -2294,7 +2352,7 @@ private void clearLearnMoreComposite(Composite learnMoreComposite) { child.dispose(); } } - + /** * Add Learn More sections to composite (Risk, Cause, General Recommendations) * @@ -2305,11 +2363,11 @@ private void clearLearnMoreComposite(Composite learnMoreComposite) { */ private void addLearnMoreSectionsToComposite(Composite composite, String title, String description) { Label titleLabel = new Label(composite, SWT.WRAP); - titleLabel.setText(title); + titleLabel.setText(title); titleLabel.setFont(boldFont); StyledText descriptionLabel = new StyledText(composite, SWT.WRAP); - descriptionLabel.setText(description); + descriptionLabel.setText(description); GridData descriptionLayout = new GridData(GridData.FILL_HORIZONTAL); descriptionLayout.grabExcessHorizontalSpace = true; descriptionLayout.horizontalAlignment = SWT.FILL; @@ -2319,15 +2377,18 @@ private void addLearnMoreSectionsToComposite(Composite composite, String title, descriptionLabel.setBottomMargin(20); } - /*private void populateBFLMessage(Image image, String bflMessage) { - bflLabel.setImage(image); - bflText.setText(bflMessage); - bflLabel.layout(); - bflText.requestLayout(); - - }*/ + /* + * private void populateBFLMessage(Image image, String bflMessage) { + * bflLabel.setImage(image); + * bflText.setText(bflMessage); + * bflLabel.layout(); + * bflText.requestLayout(); + * + * } + */ - private void drawIndividualAttackVectorData(Composite parent, String queryName, String groupName, List nodesList, Boolean populateBFLNode) { + private void drawIndividualAttackVectorData(Composite parent, String queryName, String groupName, + List nodesList, Boolean populateBFLNode) { if (nodesList != null && !nodesList.isEmpty()) { for (int i = 0; i < nodesList.size(); i++) { @@ -2335,7 +2396,8 @@ private void drawIndividualAttackVectorData(Composite parent, String queryName, Composite listComposite = createRowComposite(parent); - CLabel label = createRowLabel(listComposite, String.format("%s | %s", i + 1, node.getName()), populateBFLNode ? i == bflNode : false); + CLabel label = createRowLabel(listComposite, String.format("%s | %s", i + 1, node.getName()), + populateBFLNode ? i == bflNode : false); Link attackVectorValueLinkText = createRowLink(listComposite, String.format("%s[%d,%d]", node.getFileName(), node.getLine(), node.getColumn()), @@ -2353,56 +2415,61 @@ public void handleEvent(Event event) { } } - /*private void populateBFLNode(Composite parent, DisplayModel selectedItem) { - - Job job = new Job("Loading BFL node") { - - Composite itemComposite; - - @Override - protected IStatus run(IProgressMonitor arg0) { - - try { - bflNode = DataProvider.getInstance().getBestFixLocation(UUID.fromString(currentScanId), - selectedItem.getResult().getData().getQueryId(), - selectedItem.getResult().getData().getNodes()); - String queryName = selectedItem.getResult().getData().getQueryName(); - String groupName = selectedItem.getResult().getData().getGroup(); - List nodesList = selectedItem.getResult().getData().getNodes(); - - sync.asyncExec(() -> { - if (bflNode != -1) { - parent.dispose(); - itemComposite = createAttackVectorComposite(); - populateBFLMessage(BFL, PluginConstants.BFL_FOUND); - drawIndividualAttackVectorData(itemComposite, queryName, groupName, nodesList, true); - } else { - populateBFLMessage(null, PluginConstants.BFL_NOT_FOUND); - } - - }); - } catch (Exception e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BEST_FIX_LOCATION, e.getMessage()), e); - } - return Status.OK_STATUS; - } - - }; - - job.schedule(); - }*/ + /* + * private void populateBFLNode(Composite parent, DisplayModel selectedItem) { + * + * Job job = new Job("Loading BFL node") { + * + * Composite itemComposite; + * + * @Override + * protected IStatus run(IProgressMonitor arg0) { + * + * try { + * bflNode = + * DataProvider.getInstance().getBestFixLocation(UUID.fromString(currentScanId), + * selectedItem.getResult().getData().getQueryId(), + * selectedItem.getResult().getData().getNodes()); + * String queryName = selectedItem.getResult().getData().getQueryName(); + * String groupName = selectedItem.getResult().getData().getGroup(); + * List nodesList = selectedItem.getResult().getData().getNodes(); + * + * sync.asyncExec(() -> { + * if (bflNode != -1) { + * parent.dispose(); + * itemComposite = createAttackVectorComposite(); + * populateBFLMessage(BFL, PluginConstants.BFL_FOUND); + * drawIndividualAttackVectorData(itemComposite, queryName, groupName, + * nodesList, true); + * } else { + * populateBFLMessage(null, PluginConstants.BFL_NOT_FOUND); + * } + * + * }); + * } catch (Exception e) { + * CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BEST_FIX_LOCATION, + * e.getMessage()), e); + * } + * return Status.OK_STATUS; + * } + * + * }; + * + * job.schedule(); + * } + */ - private void drawVulnerabilityLocation(DisplayModel selectedItem) { + private void drawVulnerabilityLocation(DisplayModel selectedItem) { ScrolledComposite sc = new ScrolledComposite(attackVectorCompositePanel, SWT.H_SCROLL | SWT.V_SCROLL); Composite child = new Composite(sc, SWT.NONE); child.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, true)); - child.setLayout(new GridLayout(1, false)); + child.setLayout(new GridLayout(1, false)); child.setBackground(attackVectorCompositePanel.getBackground()); drawAttackVectorTitle(child, PluginConstants.LOCATION); drawIndividualLocationData(child, selectedItem); - + sc.setContent(child); sc.setMinSize(child.computeSize(SWT.DEFAULT, SWT.DEFAULT)); sc.setExpandHorizontal(true); @@ -2457,7 +2524,7 @@ private CLabel createRowLabel(Composite rowComposite, String text, Boolean isBfl label.setFont(boldFont); label.setText(text); label.requestLayout(); - + return label; } @@ -2710,7 +2777,7 @@ protected IStatus run(IProgressMonitor arg0) { // Clear vulnerabilities from Problems View PluginUtils.clearVulnerabilitiesFromProblemsView(); - + toolBarActions.refreshToolbar(); } @@ -2789,7 +2856,7 @@ public void run() { } } }); - + return Status.OK_STATUS; } @@ -2809,7 +2876,8 @@ private void enablePluginFields(boolean enableBranchCombobox) { for (Action action : toolBarActions.getToolBarActions()) { String actionName = action.getId(); - if (actionName.equals(ActionName.GROUP_BY_SEVERITY.name()) && !actionName.equals(ActionName.GROUP_BY_QUERY_NAME.name())) { + if (actionName.equals(ActionName.GROUP_BY_SEVERITY.name()) + && !actionName.equals(ActionName.GROUP_BY_QUERY_NAME.name())) { continue; } @@ -2898,31 +2966,33 @@ private List getProjects() { return projectList; } - + /** * Update scan button with proper tooltip * * @param enabled */ - private void updateStartScanButton(boolean enabled) { - if(enabled) { - String runningScanId = GlobalSettings.getFromPreferences(GlobalSettings.PARAM_RUNNING_SCAN_ID, PluginConstants.EMPTY_STRING); - boolean isScanRunning = StringUtils.isNoneEmpty(runningScanId); - boolean branchSelected = StringUtils.isNotBlank(GlobalSettings.getFromPreferences(GlobalSettings.PARAM_BRANCH, PluginConstants.EMPTY_STRING)); - + private void updateStartScanButton(boolean enabled) { + if (enabled) { + String runningScanId = GlobalSettings.getFromPreferences(GlobalSettings.PARAM_RUNNING_SCAN_ID, + PluginConstants.EMPTY_STRING); + boolean isScanRunning = StringUtils.isNoneEmpty(runningScanId); + boolean branchSelected = StringUtils.isNotBlank( + GlobalSettings.getFromPreferences(GlobalSettings.PARAM_BRANCH, PluginConstants.EMPTY_STRING)); + toolBarActions.getStartScanAction().setEnabled(!isScanRunning && branchSelected); } else { toolBarActions.getStartScanAction().setEnabled(false); } } - + /** * Store the user entered value and the caret position * * @param projectList * @param searchText */ - private void preservCaretposition( List projectList, String searchText) { + private void preservCaretposition(List projectList, String searchText) { int caretPos = projectComboViewer.getCombo().getCaretPosition(); projectComboViewer.setInput(projectList); PluginUtils.setTextForComboViewer(projectComboViewer, searchText); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java index 70b98a72..c37b254a 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/filters/FilterState.java @@ -71,28 +71,28 @@ public static void setState(Severity severity) { return; } switch (severity) { - case CRITICAL: - critical = !critical; - GlobalSettings.storeInPreferences(Severity.CRITICAL.name(), String.valueOf(critical)); - break; - case HIGH: - high = !high; - GlobalSettings.storeInPreferences(Severity.HIGH.name(), String.valueOf(high)); - break; - case MEDIUM: - medium = !medium; - GlobalSettings.storeInPreferences(Severity.MEDIUM.name(), String.valueOf(medium)); - break; - case LOW: - low = !low; - GlobalSettings.storeInPreferences(Severity.LOW.name(), String.valueOf(low)); - break; - case INFO: - info = !info; - GlobalSettings.storeInPreferences(Severity.INFO.name(), String.valueOf(info)); - break; - default: - break; + case CRITICAL: + critical = !critical; + GlobalSettings.storeInPreferences(Severity.CRITICAL.name(), String.valueOf(critical)); + break; + case HIGH: + high = !high; + GlobalSettings.storeInPreferences(Severity.HIGH.name(), String.valueOf(high)); + break; + case MEDIUM: + medium = !medium; + GlobalSettings.storeInPreferences(Severity.MEDIUM.name(), String.valueOf(medium)); + break; + case LOW: + low = !low; + GlobalSettings.storeInPreferences(Severity.LOW.name(), String.valueOf(low)); + break; + case INFO: + info = !info; + GlobalSettings.storeInPreferences(Severity.INFO.name(), String.valueOf(info)); + break; + default: + break; } } @@ -104,57 +104,57 @@ public static void setGroupingMode(GroupingMode mode) { return; } switch (mode) { - case SEVERITY: - groupBySeverity = !groupBySeverity; - GlobalSettings.storeInPreferences(GroupingMode.SEVERITY.name(), String.valueOf(groupBySeverity)); - break; - case QUERY_NAME: - groupByQueryName = !groupByQueryName; - GlobalSettings.storeInPreferences(GroupingMode.QUERY_NAME.name(), String.valueOf(groupByQueryName)); - break; - case STATE_NAME: - groupByStateName = !groupByStateName; - GlobalSettings.storeInPreferences(GroupingMode.STATE_NAME.name(), String.valueOf(groupByStateName)); - break; - default: - break; + case SEVERITY: + groupBySeverity = !groupBySeverity; + GlobalSettings.storeInPreferences(GroupingMode.SEVERITY.name(), String.valueOf(groupBySeverity)); + break; + case QUERY_NAME: + groupByQueryName = !groupByQueryName; + GlobalSettings.storeInPreferences(GroupingMode.QUERY_NAME.name(), String.valueOf(groupByQueryName)); + break; + case STATE_NAME: + groupByStateName = !groupByStateName; + GlobalSettings.storeInPreferences(GroupingMode.STATE_NAME.name(), String.valueOf(groupByStateName)); + break; + default: + break; } } public static void setFilterState(State state) { switch (state.getName()) { - case "NOT_EXPLOITABLE": - notExploitable = !notExploitable; - GlobalSettings.storeInPreferences("NOT_EXPLOITABLE", String.valueOf(notExploitable)); - break; - case "PROPOSED_NOT_EXPLOITABLE": - proposedNotExploitable = !proposedNotExploitable; - GlobalSettings.storeInPreferences("PROPOSED_NOT_EXPLOITABLE", String.valueOf(proposedNotExploitable)); - break; - case "URGENT": - urgent = !urgent; - GlobalSettings.storeInPreferences("URGENT", String.valueOf(urgent)); - break; - case "IGNORED": - ignored = !ignored; - GlobalSettings.storeInPreferences("IGNORED", String.valueOf(ignored)); - break; - case "CONFIRMED": - confirmed = !confirmed; - GlobalSettings.storeInPreferences("CONFIRMED", String.valueOf(confirmed)); - break; - case "NOT_IGNORED": - not_ignored = !not_ignored; - GlobalSettings.storeInPreferences("NOT_IGNORED", String.valueOf(not_ignored)); - break; - case "TO_VERIFY": - to_verify = !to_verify; - GlobalSettings.storeInPreferences("TO_VERIFY", String.valueOf(to_verify)); - break; - default: - // For custom states, toggle the global customState flag - setCustomStateFilter(); - break; + case "NOT_EXPLOITABLE": + notExploitable = !notExploitable; + GlobalSettings.storeInPreferences("NOT_EXPLOITABLE", String.valueOf(notExploitable)); + break; + case "PROPOSED_NOT_EXPLOITABLE": + proposedNotExploitable = !proposedNotExploitable; + GlobalSettings.storeInPreferences("PROPOSED_NOT_EXPLOITABLE", String.valueOf(proposedNotExploitable)); + break; + case "URGENT": + urgent = !urgent; + GlobalSettings.storeInPreferences("URGENT", String.valueOf(urgent)); + break; + case "IGNORED": + ignored = !ignored; + GlobalSettings.storeInPreferences("IGNORED", String.valueOf(ignored)); + break; + case "CONFIRMED": + confirmed = !confirmed; + GlobalSettings.storeInPreferences("CONFIRMED", String.valueOf(confirmed)); + break; + case "NOT_IGNORED": + not_ignored = !not_ignored; + GlobalSettings.storeInPreferences("NOT_IGNORED", String.valueOf(not_ignored)); + break; + case "TO_VERIFY": + to_verify = !to_verify; + GlobalSettings.storeInPreferences("TO_VERIFY", String.valueOf(to_verify)); + break; + default: + // For custom states, toggle the global customState flag + setCustomStateFilter(); + break; } } @@ -177,22 +177,22 @@ public static boolean isFilterStateEnabled(String state) { String normalized = state.trim().toUpperCase(); if (PREDEFINED_STATE_SET.contains(normalized)) { switch (normalized) { - case "NOT_EXPLOITABLE": - return notExploitable; - case "PROPOSED_NOT_EXPLOITABLE": - return proposedNotExploitable; - case "TO_VERIFY": - return to_verify; - case "CONFIRMED": - return confirmed; - case "URGENT": - return urgent; - case "NOT_IGNORED": - return not_ignored; - case "IGNORED": - return ignored; - default: - break; + case "NOT_EXPLOITABLE": + return notExploitable; + case "PROPOSED_NOT_EXPLOITABLE": + return proposedNotExploitable; + case "TO_VERIFY": + return to_verify; + case "CONFIRMED": + return confirmed; + case "URGENT": + return urgent; + case "NOT_IGNORED": + return not_ignored; + case "IGNORED": + return ignored; + default: + break; } } else { // [AST-92100] Not a predefined state, check if this custom state is enabled @@ -210,18 +210,18 @@ public static boolean isSeverityEnabled(String severity) { } try { switch (Severity.getSeverity(severity)) { - case CRITICAL: - return critical; - case HIGH: - return high; - case MEDIUM: - return medium; - case LOW: - return low; - case INFO: - return info; - default: - break; + case CRITICAL: + return critical; + case HIGH: + return high; + case MEDIUM: + return medium; + case LOW: + return low; + case INFO: + return info; + default: + break; } } catch (IllegalArgumentException e) { // Invalid severity string @@ -238,14 +238,14 @@ public static boolean isGroupingModeEnabled(GroupingMode mode) { return false; } switch (mode) { - case SEVERITY: - return groupBySeverity; - case QUERY_NAME: - return groupByQueryName; - case STATE_NAME: - return groupByStateName; - default: - return false; + case SEVERITY: + return groupBySeverity; + case QUERY_NAME: + return groupByQueryName; + case STATE_NAME: + return groupByStateName; + default: + return false; } } diff --git a/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java b/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java index 1a640152..79a19f46 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java +++ b/common-lib/src/com/checkmarx/eclipse/common/enums/Severity.java @@ -3,7 +3,8 @@ /** * Severity levels for security findings. * - * Note: UI grouping modes are kept separate in the plugin module (GroupingMode enum). + * Note: UI grouping modes are kept separate in the plugin module (GroupingMode + * enum). * This enum is limited to actual severity levels for the shared contract. */ public enum Severity { @@ -12,7 +13,8 @@ public enum Severity { HIGH, MEDIUM, LOW, - INFO; + INFO, + MALICIOUS; public static Severity getSeverity(String severity) { return Severity.valueOf(severity); diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java index a46eb92d..92e00f29 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java @@ -27,7 +27,7 @@ */ public class CheckmarxPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { - // Preference Keys + // 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"; @@ -45,46 +45,48 @@ public class CheckmarxPreferencePage extends PreferencePage implements IWorkbenc private Combo containersToolCombo; private boolean loggedIn; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE= "Checkmarx Developer Assist Open Source Realtime Scanner (OSS-Realtime): Activate OSS-Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE="Checkmarx Developer Assist Secret Detection Realtime Scanner: Activate Secret Detection Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE= "Checkmarx Developer Assist Containers Realtime Scanner: Activate Containers Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE= "Checkmarx Developer Assist IAC Realtime Scanner: Activate IAC Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE= "Checkmarx Developer Assist AI Secure Coding Assistant (ASCA): Activate ASCA"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX= "Checkmarx Developer Assist IAC Realtime Scanner: Containers Management Tool"; - public static final String DEVASSIST_PLUGIN_WELCOME_TITLE= "Welcome to Checkmarx Developer Assist"; - public static final String CONTAINERS_TOOL_DESCRIPTION="Select the Containers Management Tool to use for IaC scanning."; - public static final String OSS_REALTIME_CHECKBOX="Scans your manifest files as you code"; - public static final String SECRETS_REALTIME_CHECKBOX="Scans your files for potential secrets and credentials as you code"; - public static final String CONTAINERS_REALTIME_CHECKBOX="Scans your Docker files and container configurations as you code"; - public static final String IAC_REALTIME_CHECKBOX="Scans your Infrastructure as Code files as you code"; - public static final String ASCA_CHECKBOX="Scan your file as you code"; - - - - public CheckmarxPreferencePage() { - super(); - setPreferenceStore(com.checkmarx.eclipse.common.preferences.Preferences.STORE); - // Listen for preference changes to update login state. - // Critical: if user logs out in another page while this page is visible in the same - // dialog session, we need to refresh the UI to show logged-out content instead of stale - // logged-in checkboxes. Without this, performOk() would still run with stale loggedIn=true. - Preferences.STORE.addPropertyChangeListener(this::handlePreferenceChange); - } - - /** - * Called when preferences change (e.g., user logs out in another page of the same dialog). - * Re-reads the login state and updates the visible UI accordingly. - */ - private void handlePreferenceChange(PropertyChangeEvent event) { - // Re-check login state: if API key was cleared, we need to switch from - // logged-in scanner checkboxes to logged-out message - boolean isNowLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); - if (loggedIn != isNowLoggedIn) { - loggedIn = isNowLoggedIn; - } - } - - @Override + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE = "Checkmarx Developer Assist Open Source Realtime Scanner (OSS-Realtime): Activate OSS-Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE = "Checkmarx Developer Assist Secret Detection Realtime Scanner: Activate Secret Detection Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE = "Checkmarx Developer Assist Containers Realtime Scanner: Activate Containers Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE = "Checkmarx Developer Assist IAC Realtime Scanner: Activate IAC Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE = "Checkmarx Developer Assist AI Secure Coding Assistant (ASCA): Activate ASCA"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX = "Checkmarx Developer Assist IAC Realtime Scanner: Containers Management Tool"; + public static final String DEVASSIST_PLUGIN_WELCOME_TITLE = "Welcome to Checkmarx Developer Assist"; + public static final String CONTAINERS_TOOL_DESCRIPTION = "Select the Containers Management Tool to use for IaC scanning."; + public static final String OSS_REALTIME_CHECKBOX = "Scans your manifest files as you code"; + public static final String SECRETS_REALTIME_CHECKBOX = "Scans your files for potential secrets and credentials as you code"; + public static final String CONTAINERS_REALTIME_CHECKBOX = "Scans your Docker files and container configurations as you code"; + public static final String IAC_REALTIME_CHECKBOX = "Scans your Infrastructure as Code files as you code"; + public static final String ASCA_CHECKBOX = "Scan your file as you code"; + + public CheckmarxPreferencePage() { + super(); + setPreferenceStore(com.checkmarx.eclipse.common.preferences.Preferences.STORE); + // Listen for preference changes to update login state. + // Critical: if user logs out in another page while this page is visible in the + // same + // dialog session, we need to refresh the UI to show logged-out content instead + // of stale + // logged-in checkboxes. Without this, performOk() would still run with stale + // loggedIn=true. + Preferences.STORE.addPropertyChangeListener(this::handlePreferenceChange); + } + + /** + * Called when preferences change (e.g., user logs out in another page of the + * same dialog). + * Re-reads the login state and updates the visible UI accordingly. + */ + private void handlePreferenceChange(PropertyChangeEvent event) { + // Re-check login state: if API key was cleared, we need to switch from + // logged-in scanner checkboxes to logged-out message + boolean isNowLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + if (loggedIn != isNowLoggedIn) { + loggedIn = isNowLoggedIn; + } + } + + @Override protected Control createContents(Composite parent) { loggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); if (!loggedIn) { @@ -145,18 +147,19 @@ protected Control createContents(Composite parent) { containerDesc.setLayoutData(descData); containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); - containersToolCombo.setItems(new String[] { "docker", "podman"}); + containersToolCombo.setItems(new String[] { "docker", "podman" }); containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); loadValues(); return mainPanel; } - /** - * Shown instead of the scanner checkboxes when the user isn't logged in - there - * is nothing meaningful to configure until credentials are set in "Checkmarx One". - */ - private Control createLoggedOutContent(Composite parent) { + /** + * Shown instead of the scanner checkboxes when the user isn't logged in - there + * is nothing meaningful to configure until credentials are set in "Checkmarx + * One". + */ + private Control createLoggedOutContent(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(1, false); layout.marginTop = 20; @@ -184,7 +187,7 @@ public void widgetSelected(SelectionEvent e) { return composite; } - private Composite createIndentComposite(Composite parent) { + private Composite createIndentComposite(Composite parent) { Composite comp = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(1, false); layout.marginLeft = 15; @@ -228,49 +231,51 @@ protected void performDefaults() { 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 + private void createSectionHeader(Composite parent, String titleText) { + Composite headerComp = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.marginWidth = 0; + layout.marginTop = 6; + layout.marginBottom = 0; + headerComp.setLayout(layout); + headerComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + int colonIndex = titleText.indexOf(":"); + + StyledText title = new StyledText(headerComp, SWT.READ_ONLY | SWT.WRAP); + title.setText(titleText); + title.setBackground(headerComp.getBackground()); // Match background color + title.setCaret(null); // Hide text cursor + + if (colonIndex != -1 && colonIndex + 1 < titleText.length()) { + int start = colonIndex + 1; // Start right after the colon + int length = titleText.length() - start; + + StyleRange boldStyle = new StyleRange(); + boldStyle.start = start; + boldStyle.length = length; + boldStyle.fontStyle = SWT.BOLD; + + title.setStyleRange(boldStyle); + + } + } + + @Override + public void init(IWorkbench workbench) { + // Initialization if needed + } + + @Override public boolean performOk() { // Check credentials fresh, not from captured field. - // Critical: if user logged out while viewing another page within the same dialog session, - // loggedIn would be stale and we'd save/notify with false authentication status. + // Critical: if user logged out while viewing another page within the same + // dialog session, + // loggedIn would be stale and we'd save/notify with false authentication + // status. boolean isCurrentlyLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); if (!isCurrentlyLoggedIn) { return super.performOk(); @@ -297,15 +302,16 @@ public boolean performOk() { // Diagnostic: Verify what was saved CxLogger.info("[PREFS-PAGE] Saved to preference store: ASCA=" + ascaSelected + ", OSS=" + ossSelected + - ", SECRETS=" + secretsSelected + ", CONTAINERS=" + containersSelected + ", IAC=" + iacSelected); + ", SECRETS=" + secretsSelected + ", CONTAINERS=" + containersSelected + ", IAC=" + iacSelected); // Step 2: Save as user preferences (mirrors JetBrains apply() method) // This preserves user's choices if features toggle on/off later Preferences.setUserPreferences(ascaSelected, ossSelected, secretsSelected, - containersSelected, iacSelected); + containersSelected, iacSelected); CxLogger.info("[PREFS-PAGE] Saved as user preferences"); - // Step 3: Notify listeners (e.g., GlobalScannerController) about preference changes + // Step 3: Notify listeners (e.g., GlobalScannerController) about preference + // changes // The listener will update GlobalScannerController based on new preferences // This decouples CheckmarxPreferencePage from devassist-lib modules for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { diff --git a/devassist-lib/META-INF/MANIFEST.MF b/devassist-lib/META-INF/MANIFEST.MF index 71272e26..23ddc285 100644 --- a/devassist-lib/META-INF/MANIFEST.MF +++ b/devassist-lib/META-INF/MANIFEST.MF @@ -13,6 +13,7 @@ Require-Bundle: com.checkmarx.eclipse.common, org.eclipse.ui.workbench.texteditor, org.eclipse.ui.editors, org.eclipse.ui.ide, + org.eclipse.ui.genericeditor, org.eclipse.core.runtime, org.eclipse.core.resources, org.eclipse.core.commands, @@ -21,7 +22,8 @@ Require-Bundle: com.checkmarx.eclipse.common, org.eclipse.swt, org.eclipse.jgit, org.eclipse.e4.core.services, - org.eclipse.e4.ui.css.swt.theme + org.eclipse.e4.ui.css.swt.theme, + org.eclipse.jdt.ui Import-Package: org.eclipse.mylyn.commons.ui.dialogs, org.osgi.service.event;version="1.4.1" Export-Package: com.checkmarx.eclipse.devassist.backend, diff --git a/devassist-lib/icons/critical_20.svg b/devassist-lib/icons/critical_20.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/devassist-lib/icons/critical_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/critical_20_dark.svg b/devassist-lib/icons/critical_20_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/devassist-lib/icons/critical_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/high_20.svg b/devassist-lib/icons/high_20.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/devassist-lib/icons/high_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/high_20_dark.svg b/devassist-lib/icons/high_20_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/devassist-lib/icons/high_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_20.svg b/devassist-lib/icons/ignored_20.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/devassist-lib/icons/ignored_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/ignored_20_dark.svg b/devassist-lib/icons/ignored_20_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/devassist-lib/icons/ignored_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/low_20.svg b/devassist-lib/icons/low_20.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/devassist-lib/icons/low_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/low_20_dark.svg b/devassist-lib/icons/low_20_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/devassist-lib/icons/low_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/malicious_20.svg b/devassist-lib/icons/malicious_20.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/devassist-lib/icons/malicious_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/malicious_20_dark.svg b/devassist-lib/icons/malicious_20_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/devassist-lib/icons/malicious_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/medium_20.svg b/devassist-lib/icons/medium_20.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/devassist-lib/icons/medium_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/medium_20_dark.svg b/devassist-lib/icons/medium_20_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/devassist-lib/icons/medium_20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/critical.svg b/devassist-lib/icons/severity_16/critical.svg new file mode 100644 index 00000000..6e1929e8 --- /dev/null +++ b/devassist-lib/icons/severity_16/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/critical_dark.svg b/devassist-lib/icons/severity_16/critical_dark.svg new file mode 100644 index 00000000..9c89888d --- /dev/null +++ b/devassist-lib/icons/severity_16/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/high.svg b/devassist-lib/icons/severity_16/high.svg new file mode 100644 index 00000000..4c815e84 --- /dev/null +++ b/devassist-lib/icons/severity_16/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/high_dark.svg b/devassist-lib/icons/severity_16/high_dark.svg new file mode 100644 index 00000000..d9b8a81f --- /dev/null +++ b/devassist-lib/icons/severity_16/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ignored.svg b/devassist-lib/icons/severity_16/ignored.svg new file mode 100644 index 00000000..4ec04da0 --- /dev/null +++ b/devassist-lib/icons/severity_16/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ignored_dark.svg b/devassist-lib/icons/severity_16/ignored_dark.svg new file mode 100644 index 00000000..20246d56 --- /dev/null +++ b/devassist-lib/icons/severity_16/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/low.svg b/devassist-lib/icons/severity_16/low.svg new file mode 100644 index 00000000..40b203e4 --- /dev/null +++ b/devassist-lib/icons/severity_16/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/low_dark.svg b/devassist-lib/icons/severity_16/low_dark.svg new file mode 100644 index 00000000..69f9b3a6 --- /dev/null +++ b/devassist-lib/icons/severity_16/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/malicious.svg b/devassist-lib/icons/severity_16/malicious.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/severity_16/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/malicious_dark.svg b/devassist-lib/icons/severity_16/malicious_dark.svg new file mode 100644 index 00000000..32a94bd0 --- /dev/null +++ b/devassist-lib/icons/severity_16/malicious_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/medium.svg b/devassist-lib/icons/severity_16/medium.svg new file mode 100644 index 00000000..3a6cda49 --- /dev/null +++ b/devassist-lib/icons/severity_16/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/medium_dark.svg b/devassist-lib/icons/severity_16/medium_dark.svg new file mode 100644 index 00000000..5be2c823 --- /dev/null +++ b/devassist-lib/icons/severity_16/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ok.svg b/devassist-lib/icons/severity_16/ok.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/severity_16/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/ok_dark.svg b/devassist-lib/icons/severity_16/ok_dark.svg new file mode 100644 index 00000000..21fa16ef --- /dev/null +++ b/devassist-lib/icons/severity_16/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_16/unknown.svg b/devassist-lib/icons/severity_16/unknown.svg new file mode 100644 index 00000000..d63f29bf --- /dev/null +++ b/devassist-lib/icons/severity_16/unknown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_16/unknown_dark.svg b/devassist-lib/icons/severity_16/unknown_dark.svg new file mode 100644 index 00000000..a5270a2a --- /dev/null +++ b/devassist-lib/icons/severity_16/unknown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_20/critical.svg b/devassist-lib/icons/severity_20/critical.svg new file mode 100644 index 00000000..5a297484 --- /dev/null +++ b/devassist-lib/icons/severity_20/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/critical_dark.svg b/devassist-lib/icons/severity_20/critical_dark.svg new file mode 100644 index 00000000..74a7154a --- /dev/null +++ b/devassist-lib/icons/severity_20/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/high.svg b/devassist-lib/icons/severity_20/high.svg new file mode 100644 index 00000000..167be4d1 --- /dev/null +++ b/devassist-lib/icons/severity_20/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/high_dark.svg b/devassist-lib/icons/severity_20/high_dark.svg new file mode 100644 index 00000000..292e26a0 --- /dev/null +++ b/devassist-lib/icons/severity_20/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ignored.svg b/devassist-lib/icons/severity_20/ignored.svg new file mode 100644 index 00000000..f8b60d31 --- /dev/null +++ b/devassist-lib/icons/severity_20/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ignored_dark.svg b/devassist-lib/icons/severity_20/ignored_dark.svg new file mode 100644 index 00000000..06138d2a --- /dev/null +++ b/devassist-lib/icons/severity_20/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/low.svg b/devassist-lib/icons/severity_20/low.svg new file mode 100644 index 00000000..0ad469eb --- /dev/null +++ b/devassist-lib/icons/severity_20/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/low_dark.svg b/devassist-lib/icons/severity_20/low_dark.svg new file mode 100644 index 00000000..b4310c02 --- /dev/null +++ b/devassist-lib/icons/severity_20/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/malicious.svg b/devassist-lib/icons/severity_20/malicious.svg new file mode 100644 index 00000000..946f3889 --- /dev/null +++ b/devassist-lib/icons/severity_20/malicious.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/malicious_dark.svg b/devassist-lib/icons/severity_20/malicious_dark.svg new file mode 100644 index 00000000..032df876 --- /dev/null +++ b/devassist-lib/icons/severity_20/malicious_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/medium.svg b/devassist-lib/icons/severity_20/medium.svg new file mode 100644 index 00000000..4117ba0e --- /dev/null +++ b/devassist-lib/icons/severity_20/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/medium_dark.svg b/devassist-lib/icons/severity_20/medium_dark.svg new file mode 100644 index 00000000..8cd8ec41 --- /dev/null +++ b/devassist-lib/icons/severity_20/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ok.svg b/devassist-lib/icons/severity_20/ok.svg new file mode 100644 index 00000000..dc746080 --- /dev/null +++ b/devassist-lib/icons/severity_20/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_20/ok_dark.svg b/devassist-lib/icons/severity_20/ok_dark.svg new file mode 100644 index 00000000..c139bab4 --- /dev/null +++ b/devassist-lib/icons/severity_20/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/critical.svg b/devassist-lib/icons/severity_24/critical.svg new file mode 100644 index 00000000..b53aebfc --- /dev/null +++ b/devassist-lib/icons/severity_24/critical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/critical_dark.svg b/devassist-lib/icons/severity_24/critical_dark.svg new file mode 100644 index 00000000..162d5016 --- /dev/null +++ b/devassist-lib/icons/severity_24/critical_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/high.svg b/devassist-lib/icons/severity_24/high.svg new file mode 100644 index 00000000..50837da7 --- /dev/null +++ b/devassist-lib/icons/severity_24/high.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/high_dark.svg b/devassist-lib/icons/severity_24/high_dark.svg new file mode 100644 index 00000000..01ab7f2d --- /dev/null +++ b/devassist-lib/icons/severity_24/high_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ignored.svg b/devassist-lib/icons/severity_24/ignored.svg new file mode 100644 index 00000000..95180214 --- /dev/null +++ b/devassist-lib/icons/severity_24/ignored.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ignored_dark.svg b/devassist-lib/icons/severity_24/ignored_dark.svg new file mode 100644 index 00000000..a8df1cee --- /dev/null +++ b/devassist-lib/icons/severity_24/ignored_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/low.svg b/devassist-lib/icons/severity_24/low.svg new file mode 100644 index 00000000..a9e7b0ec --- /dev/null +++ b/devassist-lib/icons/severity_24/low.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/low_dark.svg b/devassist-lib/icons/severity_24/low_dark.svg new file mode 100644 index 00000000..cfdc04b9 --- /dev/null +++ b/devassist-lib/icons/severity_24/low_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/malicious.svg b/devassist-lib/icons/severity_24/malicious.svg new file mode 100644 index 00000000..9c78e5cf --- /dev/null +++ b/devassist-lib/icons/severity_24/malicious.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_24/malicious_dark.svg b/devassist-lib/icons/severity_24/malicious_dark.svg new file mode 100644 index 00000000..5635eced --- /dev/null +++ b/devassist-lib/icons/severity_24/malicious_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/devassist-lib/icons/severity_24/medium.svg b/devassist-lib/icons/severity_24/medium.svg new file mode 100644 index 00000000..fb1458c6 --- /dev/null +++ b/devassist-lib/icons/severity_24/medium.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/medium_dark.svg b/devassist-lib/icons/severity_24/medium_dark.svg new file mode 100644 index 00000000..0eb1ba32 --- /dev/null +++ b/devassist-lib/icons/severity_24/medium_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ok.svg b/devassist-lib/icons/severity_24/ok.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/devassist-lib/icons/severity_24/ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/severity_24/ok_dark.svg b/devassist-lib/icons/severity_24/ok_dark.svg new file mode 100644 index 00000000..df362347 --- /dev/null +++ b/devassist-lib/icons/severity_24/ok_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/devassist-lib/icons/star-action.svg b/devassist-lib/icons/star-action.svg new file mode 100644 index 00000000..bfc23248 --- /dev/null +++ b/devassist-lib/icons/star-action.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/devassist-lib/icons/tooltip/container.png b/devassist-lib/icons/tooltip/container.png new file mode 100644 index 00000000..15333f2a Binary files /dev/null and b/devassist-lib/icons/tooltip/container.png differ diff --git a/devassist-lib/icons/tooltip/container_dark.png b/devassist-lib/icons/tooltip/container_dark.png new file mode 100644 index 00000000..71980914 Binary files /dev/null and b/devassist-lib/icons/tooltip/container_dark.png differ diff --git a/devassist-lib/icons/tooltip/critical.png b/devassist-lib/icons/tooltip/critical.png new file mode 100644 index 00000000..5ebf58ac Binary files /dev/null and b/devassist-lib/icons/tooltip/critical.png differ diff --git a/devassist-lib/icons/tooltip/critical_dark.png b/devassist-lib/icons/tooltip/critical_dark.png new file mode 100644 index 00000000..98aff91b Binary files /dev/null and b/devassist-lib/icons/tooltip/critical_dark.png differ diff --git a/devassist-lib/icons/tooltip/cxone_assist.png b/devassist-lib/icons/tooltip/cxone_assist.png new file mode 100644 index 00000000..6c2c1434 Binary files /dev/null and b/devassist-lib/icons/tooltip/cxone_assist.png differ diff --git a/devassist-lib/icons/tooltip/cxone_assist_dark.png b/devassist-lib/icons/tooltip/cxone_assist_dark.png new file mode 100644 index 00000000..2962c7a9 Binary files /dev/null and b/devassist-lib/icons/tooltip/cxone_assist_dark.png differ diff --git a/devassist-lib/icons/tooltip/devassist_badge.png b/devassist-lib/icons/tooltip/devassist_badge.png new file mode 100644 index 00000000..2deaf922 Binary files /dev/null and b/devassist-lib/icons/tooltip/devassist_badge.png differ diff --git a/devassist-lib/icons/tooltip/devassist_badge_dark.png b/devassist-lib/icons/tooltip/devassist_badge_dark.png new file mode 100644 index 00000000..f8ee0be6 Binary files /dev/null and b/devassist-lib/icons/tooltip/devassist_badge_dark.png differ diff --git a/devassist-lib/icons/tooltip/high.png b/devassist-lib/icons/tooltip/high.png new file mode 100644 index 00000000..594c3ef3 Binary files /dev/null and b/devassist-lib/icons/tooltip/high.png differ diff --git a/devassist-lib/icons/tooltip/high_dark.png b/devassist-lib/icons/tooltip/high_dark.png new file mode 100644 index 00000000..8251b640 Binary files /dev/null and b/devassist-lib/icons/tooltip/high_dark.png differ diff --git a/devassist-lib/icons/tooltip/low.png b/devassist-lib/icons/tooltip/low.png new file mode 100644 index 00000000..d0f4bb3b Binary files /dev/null and b/devassist-lib/icons/tooltip/low.png differ diff --git a/devassist-lib/icons/tooltip/low_dark.png b/devassist-lib/icons/tooltip/low_dark.png new file mode 100644 index 00000000..545c1765 Binary files /dev/null and b/devassist-lib/icons/tooltip/low_dark.png differ diff --git a/devassist-lib/icons/tooltip/malicious.png b/devassist-lib/icons/tooltip/malicious.png new file mode 100644 index 00000000..6e145e36 Binary files /dev/null and b/devassist-lib/icons/tooltip/malicious.png differ diff --git a/devassist-lib/icons/tooltip/malicious_dark.png b/devassist-lib/icons/tooltip/malicious_dark.png new file mode 100644 index 00000000..6e145e36 Binary files /dev/null and b/devassist-lib/icons/tooltip/malicious_dark.png differ diff --git a/devassist-lib/icons/tooltip/medium.png b/devassist-lib/icons/tooltip/medium.png new file mode 100644 index 00000000..ecfb6fa4 Binary files /dev/null and b/devassist-lib/icons/tooltip/medium.png differ diff --git a/devassist-lib/icons/tooltip/medium_dark.png b/devassist-lib/icons/tooltip/medium_dark.png new file mode 100644 index 00000000..f5e88250 Binary files /dev/null and b/devassist-lib/icons/tooltip/medium_dark.png differ diff --git a/devassist-lib/icons/tooltip/package.png b/devassist-lib/icons/tooltip/package.png new file mode 100644 index 00000000..ce6048f0 Binary files /dev/null and b/devassist-lib/icons/tooltip/package.png differ diff --git a/devassist-lib/icons/tooltip/package_dark.png b/devassist-lib/icons/tooltip/package_dark.png new file mode 100644 index 00000000..899a77f1 Binary files /dev/null and b/devassist-lib/icons/tooltip/package_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/critical.png b/devassist-lib/icons/tooltip/severity_count/critical.png new file mode 100644 index 00000000..8b8a7e56 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/critical.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/critical_dark.png b/devassist-lib/icons/tooltip/severity_count/critical_dark.png new file mode 100644 index 00000000..c743dd71 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/critical_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/high.png b/devassist-lib/icons/tooltip/severity_count/high.png new file mode 100644 index 00000000..fc36e929 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/high.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/high_dark.png b/devassist-lib/icons/tooltip/severity_count/high_dark.png new file mode 100644 index 00000000..4ac2fc36 Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/high_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/low.png b/devassist-lib/icons/tooltip/severity_count/low.png new file mode 100644 index 00000000..a0574bce Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/low.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/low_dark.png b/devassist-lib/icons/tooltip/severity_count/low_dark.png new file mode 100644 index 00000000..81df61cf Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/low_dark.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/medium.png b/devassist-lib/icons/tooltip/severity_count/medium.png new file mode 100644 index 00000000..c0b5679f Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/medium.png differ diff --git a/devassist-lib/icons/tooltip/severity_count/medium_dark.png b/devassist-lib/icons/tooltip/severity_count/medium_dark.png new file mode 100644 index 00000000..296a081b Binary files /dev/null and b/devassist-lib/icons/tooltip/severity_count/medium_dark.png differ diff --git a/devassist-lib/plugin.xml b/devassist-lib/plugin.xml index 3ccc842d..08378142 100644 --- a/devassist-lib/plugin.xml +++ b/devassist-lib/plugin.xml @@ -18,6 +18,75 @@ restorable="true"> + + + + + + + + + + + + - + + + + + + presentationLayer="1"> + + + presentationLayer="8"> + + + + + + + + + + + + - - - - \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java index d7bf7b71..42a8d90b 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/GlobalScannerController.java @@ -27,8 +27,7 @@ public class GlobalScannerController { private static GlobalScannerController instance; // Global enable/disable state for each scanner - private final ConcurrentHashMap scannerState = - new ConcurrentHashMap<>(); + private final ConcurrentHashMap scannerState = new ConcurrentHashMap<>(); // Listeners notified when scanner state changes // Using CopyOnWriteArrayList for thread-safe concurrent iteration and mutation @@ -161,9 +160,10 @@ public void removeScannerStateListener(ScannerStateListener listener) { /** * Notify all listeners of a scanner state change. - * Thread-safe: listeners can register/unregister concurrently without ConcurrentModificationException. + * Thread-safe: listeners can register/unregister concurrently without + * ConcurrentModificationException. * - * @param type Changed scanner type + * @param type Changed scanner type * @param enabled New enabled state */ private void notifyScannerStateChanged(ScannerType type, boolean enabled) { @@ -188,11 +188,11 @@ public String getStateReport() { for (ScannerType type : ScannerType.values()) { boolean enabled = isScannerEnabled(type); sb.append(" ").append(type.getDisplayName()).append(": ") - .append(enabled ? "ENABLED" : "DISABLED").append("\n"); + .append(enabled ? "ENABLED" : "DISABLED").append("\n"); } sb.append(" Total Enabled: ").append(getEnabledScannerCount()).append("/") - .append(ScannerType.values().length); + .append(ScannerType.values().length); return sb.toString(); } @@ -205,7 +205,7 @@ public interface ScannerStateListener { /** * Called when a scanner's enabled state changes globally. * - * @param type Changed scanner type + * @param type Changed scanner type * @param enabled New enabled state */ void onScannerStateChanged(ScannerType type, boolean enabled); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java index 403fb31f..34206253 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/ScannerRegistry.java @@ -42,7 +42,6 @@ public ScannerRegistry(IProject project) { CxLogger.info(LOG_TAG + " Created for project: " + project.getName()); } - /** * Deregister and dispose all scanners (on project close). * Synchronized to prevent race with getScannerService() lazy creation. @@ -60,7 +59,7 @@ public void deregisterAllScanners() { CxLogger.info(LOG_TAG + "Disposed scanner: " + type); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error disposing scanner " + type + ": " + - e.getMessage()); + e.getMessage()); } }); @@ -98,7 +97,8 @@ public Object getScannerService(ScannerType type) { /** * Create a scanner instance by type. - * Creates implementations of ScannerService that delegate to the new scanner commands. + * Creates implementations of ScannerService that delegate to the new scanner + * commands. * * @param type Scanner type * @return Scanner instance @@ -109,23 +109,23 @@ private Object createScannerInstance(ScannerType type) { 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; + 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) { @@ -142,143 +142,227 @@ private Object createScannerInstance(ScannerType type) { } /** - * Inner class implementations of ScannerService that bridge to new scanner commands. + * Inner class implementations of ScannerService that bridge to new scanner + * commands. * These are minimal adapters that delegate to the proper scanner packages. */ private static class OssScannerServiceImpl implements ScannerService { private final com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand command; private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + OssScannerServiceImpl(IProject project) { this.command = new com.checkmarx.eclipse.devassist.scanners.oss.OssScannerCommand(project); this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() - .engineName("OSS") - .build(); + .engineName("OSS") + .build(); } + @Override - public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + public boolean shouldScanFile(String filePath) { + return filePath != null && !filePath.isEmpty(); + } + @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { try { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer, not a + // brand-new empty Document. A new Document() has no content, so + // getFileContent() falls back to reading the file from DISK - + // meaning unsaved edits (e.g. deleting a vulnerable line) would + // never be seen by the scanner until the file is saved. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : new org.eclipse.jface.text.Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; } catch (Exception e) { CxLogger.error("[OSS-SERVICE] Scan error: " + e.getMessage(), e); return null; } } + @Override - public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { + return config; + } + @Override - public void close() throws Exception { command.dispose(); } + public void close() throws Exception { + command.dispose(); + } } private static class SecretsScannerServiceImpl implements ScannerService { private final com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand command; private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + SecretsScannerServiceImpl(IProject project) { this.command = new com.checkmarx.eclipse.devassist.scanners.secrets.SecretsScannerCommand(project); this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() - .engineName("SECRETS") - .build(); + .engineName("SECRETS") + .build(); } + @Override - public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + public boolean shouldScanFile(String filePath) { + return filePath != null && !filePath.isEmpty(); + } + @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { try { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : new org.eclipse.jface.text.Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; } catch (Exception e) { CxLogger.error("[SECRETS-SERVICE] Scan error: " + e.getMessage(), e); return null; } } + @Override - public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { + return config; + } + @Override - public void close() throws Exception { command.dispose(); } + public void close() throws Exception { + command.dispose(); + } } private static class IacScannerServiceImpl implements ScannerService { private final com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand command; private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + IacScannerServiceImpl(IProject project) { this.command = new com.checkmarx.eclipse.devassist.scanners.iac.IacScannerCommand(project); this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() - .engineName("IAC") - .build(); + .engineName("IAC") + .build(); } + @Override - public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + public boolean shouldScanFile(String filePath) { + return filePath != null && !filePath.isEmpty(); + } + @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { try { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : new org.eclipse.jface.text.Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; } catch (Exception e) { CxLogger.error("[IAC-SERVICE] Scan error: " + e.getMessage(), e); return null; } } + @Override - public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { + return config; + } + @Override - public void close() throws Exception { command.dispose(); } + public void close() throws Exception { + command.dispose(); + } } private static class AscaScannerServiceImpl implements ScannerService { private final com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand command; private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + AscaScannerServiceImpl(IProject project) { this.command = new com.checkmarx.eclipse.devassist.scanners.asca.AscaScannerCommand(project); this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() - .engineName("ASCA") - .build(); + .engineName("ASCA") + .build(); } + @Override - public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + public boolean shouldScanFile(String filePath) { + return filePath != null && !filePath.isEmpty(); + } + @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { try { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : new org.eclipse.jface.text.Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; } catch (Exception e) { CxLogger.error("[ASCA-SERVICE] Scan error: " + e.getMessage(), e); return null; } } + @Override - public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { + return config; + } + @Override - public void close() throws Exception { command.dispose(); } + public void close() throws Exception { + command.dispose(); + } } private static class ContainerScannerServiceImpl implements ScannerService { private final com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand command; private final com.checkmarx.eclipse.devassist.common.ScannerConfig config; + ContainerScannerServiceImpl(IProject project) { this.command = new com.checkmarx.eclipse.devassist.scanners.containers.ContainerScannerCommand(project); this.config = com.checkmarx.eclipse.devassist.common.ScannerConfig.builder() - .engineName("CONTAINERS") - .build(); + .engineName("CONTAINERS") + .build(); } + @Override - public boolean shouldScanFile(String filePath) { return filePath != null && !filePath.isEmpty(); } + public boolean shouldScanFile(String filePath) { + return filePath != null && !filePath.isEmpty(); + } + @Override public com.checkmarx.eclipse.devassist.common.ScanResult scan(String filePath) { try { - var result = command.scan(filePath, new org.eclipse.jface.text.Document()); + // ✅ CRITICAL: Use the LIVE (possibly unsaved) editor buffer - see + // the identical fix/comment in OssScannerServiceImpl.scan() above. + org.eclipse.jface.text.IDocument liveDocument = com.checkmarx.eclipse.devassist.utils.DevAssistUtils + .getLiveDocumentForFile(filePath); + var result = command.scan(filePath, + liveDocument != null ? liveDocument : new org.eclipse.jface.text.Document()); return (com.checkmarx.eclipse.devassist.common.ScanResult) (Object) result; } catch (Exception e) { CxLogger.error("[CONTAINER-SERVICE] Scan error: " + e.getMessage(), e); return null; } } + @Override - public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { return config; } + public com.checkmarx.eclipse.devassist.common.ScannerConfig getConfig() { + return config; + } + @Override - public void close() throws Exception { command.dispose(); } + public void close() throws Exception { + command.dispose(); + } } /** @@ -316,8 +400,8 @@ public boolean isDisposed() { */ public String getStatistics() { return "Project: " + project.getName() + - ", Scanners: " + scanners.size() + - ", Disposed: " + disposed; + ", Scanners: " + scanners.size() + + ", Disposed: " + disposed; } /** @@ -342,4 +426,3 @@ public String getDisplayName() { } } } - diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java index 13ff1356..20260927 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/CheckmarxEditorListener.java @@ -48,7 +48,7 @@ public class CheckmarxEditorListener implements IPartListener2 { private final Map activeScanJobs = new HashMap<>(); public CheckmarxEditorListener() { - + } /** @@ -77,7 +77,8 @@ public void partOpened(IWorkbenchPartReference partRef) { /** * Called when an editor is activated. - * Setup scanning if not done, or trigger rescan if switching to an already-open tab. + * Setup scanning if not done, or trigger rescan if switching to an already-open + * tab. */ @Override public void partActivated(IWorkbenchPartReference partRef) { @@ -92,7 +93,7 @@ public void partActivated(IWorkbenchPartReference partRef) { if (activeListeners.containsKey(documentId)) { RealTimeScanJob scanJob = activeScanJobs.get(documentId); if (scanJob != null) { - + scanJob.reschedule(0); } return; @@ -146,13 +147,12 @@ private void setupRealtimeScanning(IEditorPart editor) { // Check if we've already set up scanning for this document if (activeListeners.containsKey(documentId)) { - + return; } // Get file name for logging String fileName = extractFileNameFromEditor(editor); - // Log to Eclipse Error Log String message = "User opened the file: " + fileName; @@ -170,11 +170,13 @@ private void setupRealtimeScanning(IEditorPart editor) { 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")); + scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project + .getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", + "scan-scheduler")); } } catch (Exception e) { - + } } @@ -189,17 +191,16 @@ private void setupRealtimeScanning(IEditorPart editor) { activeListeners.put(documentId, docListener); activeScanJobs.put(documentId, scanJob); - - // **CRITICAL FIX: Apply cached decorations if findings exist for this file** // JetBrains pattern: when editor opens, apply cached decorations immediately - // This fixes the issue where decorations don't appear if editor wasn't open during scan + // This fixes the issue where decorations don't appear if editor wasn't open + // during scan applyCachedDecorationsForFile(file, document); // **CRITICAL FIX: Trigger initial scan when file is opened** // JetBrains pattern: scan on file open, then on keystroke debounce // Without this, opening a file doesn't trigger any scan — only edits do - + scanJob.reschedule(0); } catch (Exception e) { @@ -232,7 +233,6 @@ private void cleanupRealtimeScanning(IEditorPart editor) { try { document.removeDocumentListener(listener); listener.dispose(); - } catch (Exception e) { System.err.println("[REALTIME] Error removing document listener: " + e.getMessage()); } @@ -252,9 +252,10 @@ private void cleanupRealtimeScanning(IEditorPart editor) { try { org.eclipse.core.resources.IProject project = file.getProject(); if (project != null) { - com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = - (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project.getSessionProperty( - new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "scan-scheduler")); + com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project + .getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", + "scan-scheduler")); if (scheduler != null) { scheduler.cancelScheduledInspection(file); } @@ -287,7 +288,8 @@ private IDocument getDocumentFromEditor(IEditorPart editor) { } } - // Try method 2: Adapter pattern (for MavenPomEditor and other non-ITextEditor editors) + // Try method 2: Adapter pattern (for MavenPomEditor and other non-ITextEditor + // editors) try { ITextEditor textEditor = editor.getAdapter(ITextEditor.class); if (textEditor != null) { @@ -333,8 +335,8 @@ private String extractFileNameFromEditor(IEditorPart editor) { 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(); + org.eclipse.ui.part.FileEditorInput fileInput = (org.eclipse.ui.part.FileEditorInput) editor + .getEditorInput(); return fileInput.getFile(); } } catch (Exception e) { @@ -350,7 +352,7 @@ private org.eclipse.core.resources.IFile extractFileFromEditor(IEditorPart edito * 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 file the Eclipse IFile being opened * @param document the document for the file */ private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, IDocument document) { @@ -367,24 +369,23 @@ private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file } // Get cached findings for this file - ProblemHolderService problemHolder = - (ProblemHolderService) project.getSessionProperty( + 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); + java.util.List cachedIssues = problemHolder + .getScanIssuesByFile(filePath); if (cachedIssues == null || cachedIssues.isEmpty()) { - + return; } // Apply decorations for cached findings - + ProblemDecorator.decorateEditor(file, cachedIssues); } catch (Exception e) { @@ -396,19 +397,24 @@ private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file // Implement other IPartListener2 methods (not used for real-time scanning) @Override - public void partBroughtToTop(IWorkbenchPartReference partRef) {} + public void partBroughtToTop(IWorkbenchPartReference partRef) { + } @Override - public void partDeactivated(IWorkbenchPartReference partRef) {} + public void partDeactivated(IWorkbenchPartReference partRef) { + } @Override - public void partHidden(IWorkbenchPartReference partRef) {} + public void partHidden(IWorkbenchPartReference partRef) { + } @Override - public void partVisible(IWorkbenchPartReference partRef) {} + public void partVisible(IWorkbenchPartReference partRef) { + } @Override - public void partInputChanged(IWorkbenchPartReference partRef) {} + public void partInputChanged(IWorkbenchPartReference partRef) { + } /** * Trigger an immediate rescan of every currently open editor with real-time diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java index 92acd39b..03f98cd6 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -37,14 +37,14 @@ public class ProjectLifecycleListener implements IResourceChangeListener, IProje private final Set initializedProjects = ConcurrentHashMap.newKeySet(); /** - * Register this listener with Eclipse workspace and process existing open projects. + * 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 - ); + this, + IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.POST_CHANGE); CxLogger.info(LOG_TAG + " ✓ Registered"); // FIX 1: Run immediate initialization for projects ALREADY open on IDE startup @@ -99,7 +99,7 @@ private void initExistingProjects() { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (IProject project : projects) { if (project.isOpen() && !isInitialized(project)) { - + onProjectOpen(project); } } @@ -127,7 +127,8 @@ public void resourceChanged(IResourceChangeEvent event) { } return; } - // FIX 2: Inspect IResourceDelta to catch when a closed project is opened manually + // 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(); @@ -153,13 +154,16 @@ public void resourceChanged(IResourceChangeEvent event) { private void onProjectOpen(IProject project) { String projectName = project.getName(); - if (projectName.length() > 26) projectName = projectName.substring(0, 26); + if (projectName.length() > 26) + projectName = projectName.substring(0, 26); try { if (!isUserAuthenticated()) { return; } - // Atomically mark as initialized: if add() returns false, another thread beat us to it. - // This prevents duplicate ScannerRegistry, ProblemHolderService, and workspace-scan jobs. + // Atomically mark as initialized: if add() returns false, another thread beat + // us to it. + // This prevents duplicate ScannerRegistry, ProblemHolderService, and + // workspace-scan jobs. if (!initializedProjects.add(projectName)) { return; } @@ -177,7 +181,7 @@ private void onProjectOpen(IProject project) { initializedProjects.remove(projectName); e.printStackTrace(); CxLogger.error(LOG_TAG + " Error initializing project " + - projectName + ": " + e.getMessage(), e); + projectName + ": " + e.getMessage(), e); } } @@ -212,7 +216,8 @@ private void onProjectClose(IProject project) { } try { - ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty(PROBLEM_HOLDER_KEY); + ProblemHolderService problemHolder = (ProblemHolderService) project + .getSessionProperty(PROBLEM_HOLDER_KEY); if (problemHolder != null) { problemHolder.clearAll(); CxLogger.info(LOG_TAG + " ✓ Result cache cleared"); @@ -222,7 +227,8 @@ private void onProjectClose(IProject project) { } try { - DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty(STATE_HOLDER_KEY); + DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project + .getSessionProperty(STATE_HOLDER_KEY); if (stateHolder != null) { stateHolder.clearAll(); CxLogger.info(LOG_TAG + " ✓ State holder cleared"); @@ -248,62 +254,62 @@ public String getStatistics() { } private void startWorkspaceFileScanning(IProject project) { - Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - monitor.beginTask("Scanning manifest, IaC, and container files...", 3); - - // Check if job was cancelled or project closed before starting - if (monitor.isCanceled() || !project.isOpen()) { - return Status.CANCEL_STATUS; - } - - scanManifestFiles(project); - monitor.worked(1); - - if (monitor.isCanceled() || !project.isOpen()) { - return Status.CANCEL_STATUS; - } - - scanIacFiles(project); - monitor.worked(1); - - if (monitor.isCanceled() || !project.isOpen()) { - return Status.CANCEL_STATUS; - } - - scanContainerFiles(project); - monitor.worked(1); - - return Status.OK_STATUS; - - } catch (Exception e) { - e.printStackTrace(); - return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); - } finally { - monitor.done(); - } - } - }; - - try { - // Store job reference in session property so onProjectClose() can cancel it - project.setSessionProperty(WORKSPACE_SCAN_JOB_KEY, scanJob); - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Error storing workspace scan job: " + e.getMessage()); - } - // Run as a background job so it doesn't block the IDE - scanJob.setPriority(Job.BUILD); - scanJob.schedule(); + Job scanJob = new Job("Checkmarx Workspace Scanner (" + project.getName() + ")") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Scanning manifest, IaC, and container files...", 3); + + // Check if job was cancelled or project closed before starting + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanManifestFiles(project); + monitor.worked(1); + + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanIacFiles(project); + monitor.worked(1); + + if (monitor.isCanceled() || !project.isOpen()) { + return Status.CANCEL_STATUS; + } + + scanContainerFiles(project); + monitor.worked(1); + + return Status.OK_STATUS; + + } catch (Exception e) { + e.printStackTrace(); + return new Status(IStatus.ERROR, PLUGIN_ID, "Error scanning workspace files", e); + } finally { + monitor.done(); + } + } + }; + + try { + // Store job reference in session property so onProjectClose() can cancel it + project.setSessionProperty(WORKSPACE_SCAN_JOB_KEY, scanJob); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error storing workspace scan job: " + e.getMessage()); + } + // Run as a background job so it doesn't block the IDE + scanJob.setPriority(Job.BUILD); + scanJob.schedule(); } private void scanManifestFiles(IProject project) { String[] manifestPatterns = { - "pom.xml", "package.json", "package-lock.json", "npm-shrinkwrap.json", - "go.mod", "go.sum", "requirements.txt", "Pipfile", "Pipfile.lock", "setup.py", - "Gemfile", "Gemfile.lock", "Cargo.toml", "Cargo.lock", "composer.json", "composer.lock", - "packages.config", ".csproj", "yarn.lock" + "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"); } @@ -315,20 +321,20 @@ private void scanIacFiles(IProject project) { private void scanContainerFiles(IProject project) { String[] containerPatterns = { - "Dockerfile", "dockerfile", "docker-compose.yaml", "docker-compose.yml", ".dockerignore" + "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")); + new QualifiedName(PLUGIN_ID, "scanner-registry")); DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( - new QualifiedName(PLUGIN_ID, "state-holder")); + new QualifiedName(PLUGIN_ID, "state-holder")); ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( - new QualifiedName(PLUGIN_ID, "problem-holder")); + new QualifiedName(PLUGIN_ID, "problem-holder")); if (registry == null || stateHolder == null || problemHolder == null) { return; @@ -346,7 +352,8 @@ private void findAndScanFiles(IProject project, String[] patterns, String fileTy boolean matches = false; for (String pattern : patterns) { - if (fileName.equals(pattern.toLowerCase()) || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { + if (fileName.equals(pattern.toLowerCase()) + || filePath.toLowerCase().endsWith(pattern.toLowerCase())) { matches = true; break; } @@ -369,4 +376,3 @@ private void findAndScanFiles(IProject project, String[] patterns, String fileTy } } } - diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java index 7cebd6c3..bc624b96 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java @@ -94,13 +94,11 @@ protected IStatus run(IProgressMonitor monitor) { try { // Check if file still exists and is accessible if (file == null || !file.exists()) { - return Status.CANCEL_STATUS; } // Check if the job was cancelled while waiting if (monitor.isCanceled()) { - return Status.CANCEL_STATUS; } @@ -114,7 +112,6 @@ protected IStatus run(IProgressMonitor monitor) { try { org.eclipse.core.resources.IProject project = file.getProject(); if (project == null || !project.isOpen()) { - return Status.OK_STATUS; } @@ -154,8 +151,8 @@ protected IStatus run(IProgressMonitor monitor) { // Pass progress monitor into scanFile to support cancellation during scan // execution - java.util.List issues = scanManager.scanFile(filePath, - monitor); + com.checkmarx.eclipse.devassist.common.ScanManager.ScanOutcome outcome = scanManager + .scanFileWithOutcome(filePath, monitor); // Re-check cancellation status right before updating UI/markers to avoid // publishing stale results @@ -163,9 +160,20 @@ protected IStatus run(IProgressMonitor monitor) { return Status.CANCEL_STATUS; } - // Publish results to UI - if (issues != null && !issues.isEmpty()) { - com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, issues); + // ✅ CRITICAL FIX: Publish results whenever a REAL scan ran, even if the + // result is empty. When a vulnerable line is DELETED, the scan returns + // 0 issues - if we skip publishResults() here, the old findings never + // get cleared from ProblemHolderService, so CxFindingsView and editor + // annotations are never updated (stale data). + // + // But we must NOT publish when the cycle was merely SKIPPED (file + // unchanged since last scan - e.g. a hover-triggered editor + // reactivation rescheduling this job with nothing actually different). + // Publishing an empty list in that case would incorrectly wipe out + // valid, still-current results/annotations for this file. + if (outcome.isScanned()) { + com.checkmarx.eclipse.devassist.backend.result.ResultPublisher.publishResults(file, + outcome.getIssues()); } } catch (Exception e) { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java index 8d671d8b..f3e19940 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/result/ResultPublisher.java @@ -2,9 +2,11 @@ import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.text.IDocument; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; import com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder; import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; @@ -21,7 +23,8 @@ * * Responsibilities: * - Update custom Findings View with scan results - * - Render editor decorations (gutter icons, underlines) for Findings Window issues + * - 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. @@ -41,7 +44,7 @@ public class ResultPublisher { * 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 file File that was scanned * @param scanIssues Issues found by scanners */ public static void publishResults(IFile file, List scanIssues) { @@ -50,14 +53,12 @@ public static void publishResults(IFile file, List scanIssues) { } try { // Step 1: Update Findings View (try to display immediately if view is open) - + updateFindingsView(file, scanIssues); - // Step 2: Create problem descriptors via DevAssistInspectionMgr - + createAndRenderDecorations(file, scanIssues); - } catch (Exception e) { System.err.println(LOG_TAG + " [ERROR] " + e.getMessage()); @@ -69,61 +70,59 @@ public static void publishResults(IFile file, List scanIssues) { /** * Update Findings View with scan results. * - * @param file File that was scanned - * @param scanIssues Issues to display + * ✅ CRITICAL: `scanIssues` here always represents the COMPLETE, current set + * of issues for this file across every applicable/enabled engine - because + * {@link com.checkmarx.eclipse.devassist.common.ScanManager#scanFileWithOutcome} + * runs every applicable scanner for the file in a single pass and this method + * is only invoked by callers that just performed (or confirmed) such a real + * scan cycle (see {@link RealTimeScanJob}, which gates this call on + * {@code ScanOutcome.isScanned()}). + * + * Because of that, this is a full REPLACE of the file's cached issues, not a + * per-engine merge/remove. This correctly handles the case where a vulnerable + * line is deleted (scanIssues becomes empty -> cache is fully cleared for + * this file) without needing to infer which engine produced which result. + * + * @param file File that was scanned + * @param scanIssues Complete, current issue list for this file (may be empty) */ private static void updateFindingsView(IFile file, List scanIssues) { try { - 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 + // 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( + 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); - - } + // Full replace: this cycle's scanIssues list IS the complete truth for + // this file. If it's empty, every previously-cached issue for this + // file (from any engine) is correctly dropped. This also publishes + // the ISSUES_UPDATED_TOPIC event that CxFindingsView listens to. + problemHolder.addScanIssues(filePath, scanIssues); + CxLogger.info(LOG_TAG + " Updated cache for " + filePath + " with " + scanIssues.size() + + " issues"); - // Step 2: Add new results from THIS scanner engine - problemHolder.mergeScanIssues(filePath, scanIssues); - } else { - + CxLogger.warning(LOG_TAG + " ProblemHolderService not initialized for project"); } } } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error updating findings view: " + e.getMessage(), e); e.printStackTrace(); } } @@ -137,15 +136,14 @@ private static void updateFindingsView(IFile file, List scanIssues) { * 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 + * ✅ CRITICAL: Always processes results, even if empty. + * When scan returns 0 issues, we MUST clear old decorations/annotations. + * + * @param file File that was scanned + * @param scanIssues Issues to process (may be empty) */ private static void createAndRenderDecorations(IFile file, List scanIssues) { try { - if (scanIssues.isEmpty()) { - return; - } - org.eclipse.swt.widgets.Display display = PlatformUI.getWorkbench().getDisplay(); if (display == null || display.isDisposed()) { return; @@ -161,15 +159,17 @@ private static void createAndRenderDecorations(IFile file, List scanI try { // Get registry and state holder from session properties ScannerRegistry registry = (ScannerRegistry) project.getSessionProperty( - new QualifiedName("com.checkmarx.eclipse.plugin", "scanner-registry")); + new QualifiedName("com.checkmarx.eclipse.plugin", "scanner-registry")); DevAssistScanStateHolder stateHolder = (DevAssistScanStateHolder) project.getSessionProperty( - new QualifiedName("com.checkmarx.eclipse.plugin", "state-holder")); + new QualifiedName("com.checkmarx.eclipse.plugin", "state-holder")); ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( - new QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + 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) + ")"); + 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; @@ -179,17 +179,18 @@ private static void createAndRenderDecorations(IFile file, List scanI 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()); + .filePath(filePath) + .document(document) + .scanIssueList(scanIssues) + .problemHolderService(problemHolder) + .problemDecorator(new ProblemDecorator()); // Create problem descriptors via DevAssistInspectionMgr DevAssistInspectionMgr mgr = new DevAssistInspectionMgr(registry, stateHolder); mgr.startScanAndCreateProblemDescriptors(builder); - CxLogger.info(LOG_TAG + " Problem descriptors created via DevAssistInspectionMgr for " + scanIssues.size() + " issues"); + 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()); @@ -197,7 +198,8 @@ private static void createAndRenderDecorations(IFile file, List scanI try { ProblemDecorator.decorateEditor(file, scanIssues); } catch (Exception fallbackError) { - CxLogger.error(LOG_TAG + " Fallback decoration also failed: " + fallbackError.getMessage(), fallbackError); + CxLogger.error(LOG_TAG + " Fallback decoration also failed: " + fallbackError.getMessage(), + fallbackError); } } }); @@ -208,11 +210,14 @@ private static void createAndRenderDecorations(IFile file, List scanI } /** - * Get the IDocument for a file, preferring the live editor's document (so unsaved + * 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. + * 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 @@ -222,9 +227,10 @@ private static org.eclipse.jface.text.IDocument getDocumentForFile(IFile file) { 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 (editor instanceof ITextEditor) { + ITextEditor textEditor = (ITextEditor) editor; + IDocument doc = textEditor.getDocumentProvider() + .getDocument(textEditor.getEditorInput()); if (doc != null) { return doc; } @@ -262,7 +268,8 @@ private static com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView findOp } catch (NullPointerException e) { for (var window : workbench.getWorkbenchWindows()) { page = window.getActivePage(); - if (page != null) break; + if (page != null) + break; } } @@ -271,7 +278,7 @@ private static com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView findOp } return (com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView) page - .findView(com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView.ID); + .findView(com.checkmarx.eclipse.devassist.ui.findings.CxFindingsView.ID); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error finding Findings View: " + e.getMessage()); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java index e50d196e..bb55fcd0 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/basescanner/BaseScannerCommand.java @@ -6,7 +6,8 @@ import org.eclipse.core.resources.IProject; /** - * BaseScannerCommand is an abstract implementation of the ScannerCommand interface + * 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. @@ -22,7 +23,7 @@ public abstract class BaseScannerCommand implements ScannerCommand { * Create a scanner command with configuration. * * @param project Eclipse project - * @param config Scanner configuration + * @param config Scanner configuration */ protected BaseScannerCommand(IProject project, ScannerConfig config) { this.project = project; @@ -50,7 +51,8 @@ public void register(IProject project) { /** * 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 + * 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 */ diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java index 3f76e5f4..4c5f8534 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScanManager.java @@ -45,7 +45,45 @@ public ScanManager(ScannerRegistry registry, DevAssistScanStateHolder stateHolde } /** - * Scan a file using all applicable scanners. + * Result of a scan attempt. + * + * Distinguishes a REAL scan cycle (scanners actually executed, state hash + * updated) from a SKIPPED cycle (file unchanged since last scan, or + * cancelled, or every scanner failed). Both cases can return an empty issue + * list, but only a real scan cycle means "we now know this file has these + * (possibly zero) issues" - callers must NOT treat a skipped cycle's empty + * list as "file is now clean", or they will wipe out valid cached results + * every time a no-op scan fires (e.g. editor re-activation, hover-triggered + * focus events). + */ + public static class ScanOutcome { + private final List issues; + private final boolean scanned; + + public ScanOutcome(List issues, boolean scanned) { + this.issues = issues; + this.scanned = scanned; + } + + public List getIssues() { + return issues; + } + + /** + * @return true if scanners actually ran this cycle and {@link #getIssues()} + * reflects the file's current, complete state across all applicable + * engines (even if empty). false if the cycle was skipped (file + * unchanged, cancelled, or all scanners failed) and the caller should + * leave existing cached results/decorations untouched. + */ + public boolean isScanned() { + return scanned; + } + } + + /** + * Scan a file using all applicable scanners, reporting whether a real scan + * cycle occurred. * * High-level flow: 1. Compute current file state hash 2. Check if file changed * since last scan 3. If unchanged, return cached results 4. Get all scanners @@ -54,14 +92,14 @@ public ScanManager(ScannerRegistry registry, DevAssistScanStateHolder stateHolde * results * * @param filePath Absolute file path to scan - * @param monitor progress monitor to check for cancellation during scan - * @return List of issues found by all scanners + * @param monitor progress monitor to check for cancellation during scan + * @return {@link ScanOutcome} with the issues found and whether a scan + * actually ran * @throws Exception if scan fails */ - public List scanFile(String filePath, IProgressMonitor monitor) throws Exception { + public ScanOutcome scanFileWithOutcome(String filePath, IProgressMonitor monitor) throws Exception { if (filePath == null || filePath.isEmpty()) { - - return List.of(); + return new ScanOutcome(List.of(), false); } // Handle null monitor (for backward compatibility if called without monitor) @@ -81,14 +119,15 @@ public List scanFile(String filePath, IProgressMonitor monitor) throw // failure) or every subsequent edit will be permanently BLOCKED as "already // in-flight". if (!stateHolder.hasChanged(filePath, currentStateHash)) { - return List.of(); + // Nothing changed - this is NOT a fresh scan result, it's a skipped cycle. + return new ScanOutcome(List.of(), false); } // Check cancellation before proceeding with expensive scan operations if (monitor.isCanceled()) { CxLogger.warning("[SCAN-MANAGER] Scan cancelled before starting for: " + filePath); stateHolder.markScanComplete(filePath); - return List.of(); + return new ScanOutcome(List.of(), false); } try { @@ -96,20 +135,15 @@ public List scanFile(String filePath, IProgressMonitor monitor) throw List> applicableScanners = factory.getAllSupportedScanners(filePath); - for (ScannerService scanner : applicableScanners) { - String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; - - } if (applicableScanners.isEmpty()) { // Still update state to avoid re-checking unsupported files stateHolder.updateStateHash(filePath, currentStateHash); - return List.of(); + return new ScanOutcome(List.of(), false); } // 4. Execute all scanners and merge results List allIssues = new ArrayList<>(); - int scannerIndex = 1; int successfulScanners = 0; for (ScannerService scanner : applicableScanners) { @@ -118,38 +152,35 @@ public List scanFile(String filePath, IProgressMonitor monitor) throw CxLogger.warning("[SCAN-MANAGER] Scan cancelled during scanner loop for: " + filePath); break; } - String displayName = scanner.getConfig() != null ? scanner.getConfig().getEngineName() : "Unknown"; try { var scanResult = scanner.scan(filePath); List scannerResults = scanResult != null ? scanResult.getIssues() : null; if (scannerResults != null) { - for (ScanIssue issue : scannerResults) { - } allIssues.addAll(scannerResults); } successfulScanners++; } catch (Exception e) { e.printStackTrace(); } - scannerIndex++; } // Check for cancellation before returning/publishing results if (monitor.isCanceled()) { CxLogger.warning("[SCAN-MANAGER] Scan cancelled before publishing results for: " + filePath); // Don't update state hash so file will be re-scanned when triggered again - return List.of(); + return new ScanOutcome(List.of(), false); } // 5. Update state hash only if at least one scanner succeeded // If all scanners failed, don't update hash so file will be re-scanned on next - // change + // change, and don't report this as a real scan (results are unreliable). if (successfulScanners > 0) { stateHolder.updateStateHash(filePath, currentStateHash); + return new ScanOutcome(allIssues, true); } - return allIssues; + return new ScanOutcome(List.of(), false); } finally { // Always release the in-flight marker so the next edit can trigger a scan. stateHolder.markScanComplete(filePath); @@ -157,12 +188,29 @@ public List scanFile(String filePath, IProgressMonitor monitor) throw } /** - * Scan a file using all applicable scanners (backward-compatible overload without monitor). + * Scan a file using all applicable scanners. + * + * @param filePath Absolute file path to scan + * @param monitor progress monitor to check for cancellation during scan + * @return List of issues found by all scanners + * @throws Exception if scan fails + * @deprecated Use {@link #scanFileWithOutcome(String, IProgressMonitor)} to + * distinguish a real "zero issues" result from a skipped scan + * cycle. + */ + public List scanFile(String filePath, IProgressMonitor monitor) throws Exception { + return scanFileWithOutcome(filePath, monitor).getIssues(); + } + + /** + * Scan a file using all applicable scanners (backward-compatible overload + * without monitor). * * @param filePath Absolute file path to scan * @return List of issues found by all scanners * @throws Exception if scan fails - * @deprecated Use scanFile(String filePath, IProgressMonitor monitor) for cancellation support + * @deprecated Use scanFile(String filePath, IProgressMonitor monitor) for + * cancellation support */ public List scanFile(String filePath) throws Exception { return scanFile(filePath, new org.eclipse.core.runtime.NullProgressMonitor()); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java index 82596ea8..a6e8cc48 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/common/ScannerFactory.java @@ -89,7 +89,7 @@ public List> getAllSupportedScanners(String filePath) { * Get a specific scanner by type if it supports the file. * * @param filePath File to scan - * @param type Scanner type to retrieve + * @param type Scanner type to retrieve * @return Scanner if enabled and supports file, null otherwise */ public ScannerService getScannerForFile(String filePath, ScannerType type) { @@ -113,7 +113,7 @@ public ScannerService getScannerForFile(String filePath, ScannerType type) { // Check if supports file type if (!scanner.shouldScanFile(filePath)) { CxLogger.info(LOG_TAG + " " + type.getDisplayName() + " does not support file: " + - filePath); + filePath); return null; } @@ -137,44 +137,46 @@ private ScannerService getScannerService(ScannerType type) { } } -// /** -// * 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 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. diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java index c73d06d0..7b2dc6cc 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpSettingsInjector.java @@ -92,7 +92,8 @@ public static boolean installForCopilot(String token) throws Exception { String mcpUrl = baseUrl + MCP_ENDPOINT; CxLogger.info(LOG_TAG + " MCP URL: " + mcpUrl); - CxLogger.info(LOG_TAG + " Copilot MCP preference node: " + COPILOT_UI_BUNDLE_ID + " / " + MCP_PREFERENCE_KEY); + CxLogger.info( + LOG_TAG + " Copilot MCP preference node: " + COPILOT_UI_BUNDLE_ID + " / " + MCP_PREFERENCE_KEY); boolean changed = mergeCheckmarxServer(mcpUrl, token); @@ -225,7 +226,8 @@ private static Map readServers(IEclipsePreferences node) { CxLogger.info(LOG_TAG + "Existing preference read successfully (bare form)"); return new LinkedHashMap<>(parsed); } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Failed to parse existing Copilot MCP preference, starting fresh: " + e.getMessage()); + CxLogger.warning( + LOG_TAG + " Failed to parse existing Copilot MCP preference, starting fresh: " + e.getMessage()); return new LinkedHashMap<>(); } } @@ -235,7 +237,8 @@ private static Map readServers(IEclipsePreferences node) { * {@code {"servers": {...}}}, and flushes it so it is persisted immediately * and observed by Copilot's live preference listeners. */ - private static void writeServers(IEclipsePreferences node, Map servers) throws BackingStoreException { + private static void writeServers(IEclipsePreferences node, Map servers) + throws BackingStoreException { try { if (servers.isEmpty()) { node.remove(MCP_PREFERENCE_KEY); @@ -293,7 +296,8 @@ private static String tryExtractIssuer(String rawToken) { /** * Derives AST base URL from issuer claim. - * If issuer is like https://iam.checkmarx.com, converts to https://ast.checkmarx.com + * If issuer is like https://iam.checkmarx.com, converts to + * https://ast.checkmarx.com */ private static String deriveBaseUrlFromIssuer(String issuer) { if (issuer == null || issuer.isBlank()) { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java index 2d9feb94..6411003d 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistInspectionMgr.java @@ -44,12 +44,12 @@ public class DevAssistInspectionMgr extends ScanManager { /** * Constructor accepting scanner registry and state holder. * - * @param registry Scanner registry for the project + * @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) { + ScannerRegistry registry, + com.checkmarx.eclipse.devassist.backend.DevAssistScanStateHolder stateHolder) { super(registry, stateHolder); } @@ -69,16 +69,24 @@ public DevAssistInspectionMgr( * @return Array of problem descriptors (empty if none) */ public ProblemDescriptor[] startScanAndCreateProblemDescriptors( - ProblemHelper.Builder problemHelperBuilder) { + 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 + // Use pre-scanned issues if available, otherwise scan file. + // ✅ Only fall back to a fresh scan when the caller genuinely has no + // issue list yet (null). Callers such as ResultPublisher always pass a + // real (possibly empty) list here after a completed scan cycle - an + // empty list is a legitimate "file has zero issues" result, not a + // signal to re-scan. Treating isEmpty() as "need a fresh scan" caused a + // redundant, wasted re-scan on every zero-issue publish (harmless + // since ScanManager's state-hash dedup made it a no-op, but noisy and + // misleading in logs). List allScanIssues = problemHelper.getScanIssueList(); - if (allScanIssues == null || allScanIssues.isEmpty()) { + if (allScanIssues == null) { allScanIssues = scanFile(problemHelper.getFilePath()); CxLogger.info(LOG_TAG + " Performed fresh scan for file: " + problemHelper.getFile().getName()); } else { @@ -87,7 +95,11 @@ public ProblemDescriptor[] startScanAndCreateProblemDescriptors( if (allScanIssues.isEmpty()) { CxLogger.info(LOG_TAG + " No scan issues found for: " + - problemHelper.getFile().getName()); + problemHelper.getFile().getName()); + // Clear stale cached problem descriptors for this file - otherwise a + // later getExistingProblems() lookup could resurrect descriptors for + // issues that no longer exist. + problemHelper.getProblemHolderService().removeProblemDescriptorsForFile(problemHelper.getFilePath()); decorateUIForIgnoreVulnerability(problemHelper.getFile(), allScanIssues); return new ProblemDescriptor[0]; } @@ -98,23 +110,23 @@ public ProblemDescriptor[] startScanAndCreateProblemDescriptors( // Cache issues helperWithIssues.getProblemHolderService().addScanIssues( - problemHelper.getFilePath(), allScanIssues); + 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()); + problemHelper.getFile().getName()); return new ProblemDescriptor[0]; } // Cache problem descriptors helperWithIssues.getProblemHolderService().addProblemDescriptors( - problemHelper.getFilePath(), allProblems); + problemHelper.getFilePath(), allProblems); CxLogger.info(LOG_TAG + " Created " + allProblems.size() + - " problem descriptors for: " + problemHelper.getFile().getName()); + " problem descriptors for: " + problemHelper.getFile().getName()); return allProblems.toArray(new ProblemDescriptor[0]); @@ -134,20 +146,29 @@ public ProblemDescriptor[] startScanAndCreateProblemDescriptors( * @return List of created problem descriptors */ private List createProblemDescriptorsWithDecoration( - ProblemHelper problemHelper) { + ProblemHelper problemHelper) { if (isScanIssuePresent(problemHelper.getScanIssueList())) { // Clear existing decorations ProblemDecorator.removeAllHighlighters(problemHelper.getProject()); - // Process issues with decoration enabled + // Build descriptors WITHOUT per-issue decoration: decorateUI() below + // already redraws the full, merged issue list in one pass. Passing + // isDecoratorEnabled=true here used to make ScanIssueProcessor call + // ProblemDecorator.highlightLineAddGutterIconForProblem() once per + // issue, and each of those calls clears and rebuilds ALL annotations + // for the file (ProblemDecorator.decorateEditor() unconditionally + // clears before adding) - so a 4-issue file flickered through 4 + // single-issue annotation states before the final full redraw, + // occasionally leaving the hover to sample an incomplete annotation + // model mid-flicker. List descriptors = createProblemDescriptors( - problemHelper, true); + problemHelper, false); // Decorate UI if (!descriptors.isEmpty()) { decorateUI(problemHelper.getDocument(), problemHelper.getFile(), - problemHelper.getScanIssueList()); + problemHelper.getScanIssueList()); } return descriptors; @@ -162,7 +183,7 @@ private List createProblemDescriptorsWithDecoration( * @return List of created problem descriptors */ public List createProblemDescriptorsWithoutDecoration( - ProblemHelper problemHelper) { + ProblemHelper problemHelper) { if (isScanIssuePresent(problemHelper.getScanIssueList())) { return createProblemDescriptors(problemHelper, false); @@ -178,28 +199,28 @@ public List createProblemDescriptorsWithoutDecoration( * 2. Validate and create ProblemDescriptor * 3. Collect non-null descriptors * - * @param problemHelper Helper with context and issues + * @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) { + ProblemHelper problemHelper, + boolean isDecoratorEnabled) { List descriptors = new ArrayList<>(); ScanIssueProcessor processor = new ScanIssueProcessor(problemHelper); for (ScanIssue scanIssue : problemHelper.getScanIssueList()) { ProblemDescriptor descriptor = processor.processScanIssue( - scanIssue, isDecoratorEnabled); + scanIssue, isDecoratorEnabled); if (descriptor != null) { descriptors.add(descriptor); } } CxLogger.info(LOG_TAG + " Created " + descriptors.size() + - " problem descriptors from " + problemHelper.getScanIssueList().size() + - " scan issues"); + " problem descriptors from " + problemHelper.getScanIssueList().size() + + " scan issues"); return descriptors; } @@ -210,27 +231,27 @@ private List createProblemDescriptors( * 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 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) { + 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(); + .filePath(filePath) + .document(document) + .supportedScanners(supportedEnabledScanners) + .problemHolderService(problemHolderService) + .problemDecorator(this.problemDecorator) + .build(); // Get cached issues List scanIssueList = problemHolderService.getScanIssuesByFile(filePath); @@ -253,7 +274,7 @@ public ProblemDescriptor[] getExistingProblems( decorateUI(document, file, scanIssueList); CxLogger.info(LOG_TAG + " Returning " + cachedDescriptors.size() + - " cached problem descriptors for: " + file.getName()); + " cached problem descriptors for: " + file.getName()); return cachedDescriptors.toArray(new ProblemDescriptor[0]); } @@ -261,8 +282,8 @@ public ProblemDescriptor[] getExistingProblems( /** * Decorate UI with scan results (gutter icons, underlines). * - * @param document Document to decorate - * @param file File being decorated + * @param document Document to decorate + * @param file File being decorated * @param scanIssueList Issues to show */ public void decorateUI(IDocument document, IFile file, List scanIssueList) { @@ -274,15 +295,23 @@ public void decorateUI(IDocument document, IFile file, List scanIssue } /** - * Decorate UI for ignored vulnerabilities (empty if none ignored). + * Decorate UI for ignored vulnerabilities or when NO issues found. * - * @param file File to decorate + * ✅ CRITICAL: This is called when scan returns 0 issues. + * We MUST clear old annotations/decorations from the editor. + * By calling decorateEditor() with the (empty) list, it will: + * 1. Clear old annotations via clearAnnotations() + * 2. Return early since the list is empty (nothing to add) + * + * @param file File to decorate * @param scanIssueList Issues (may be empty) */ public void decorateUIForIgnoreVulnerability(IFile file, List scanIssueList) { try { - CxLogger.info(LOG_TAG + " decorateUIForIgnoreVulnerability called for: " + file.getName()); - // TODO: Integrate with IgnoredProblemsStore when available + CxLogger.info(LOG_TAG + " decorateUIForIgnoreVulnerability called for: " + file.getName() + + " with " + scanIssueList.size() + " issues"); + // Clear decorations from editor (this also works with empty list) + ProblemDecorator.decorateEditor(file, scanIssueList); } catch (Exception e) { CxLogger.error(LOG_TAG + " Error in decorateUIForIgnoreVulnerability: " + e.getMessage(), e); } @@ -296,7 +325,7 @@ public void decorateUIForIgnoreVulnerability(IFile file, List scanIss * - Scan encounters error * - User requests reset * - * @param project Project containing file + * @param project Project containing file * @param filePath File path to reset */ public void resetEditorAndResults(IProject project, String filePath) { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java index 0a30c622..2e3f4b22 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/inspection/DevAssistScanScheduler.java @@ -13,7 +13,8 @@ import com.checkmarx.eclipse.common.utils.CxLogger; /** - * Scheduler that wraps and coordinates RealTimeScanJob for background file scanning. + * Scheduler that wraps and coordinates RealTimeScanJob for background file + * scanning. * * Responsibilities: * - Manage scheduling of real-time scans with debounce @@ -43,19 +44,24 @@ public void done(IJobChangeEvent event) { } @Override - public void aboutToRun(IJobChangeEvent event) {} + public void aboutToRun(IJobChangeEvent event) { + } @Override - public void awake(IJobChangeEvent event) {} + public void awake(IJobChangeEvent event) { + } @Override - public void running(IJobChangeEvent event) {} + public void running(IJobChangeEvent event) { + } @Override - public void scheduled(IJobChangeEvent event) {} + public void scheduled(IJobChangeEvent event) { + } @Override - public void sleeping(IJobChangeEvent event) {} + public void sleeping(IJobChangeEvent event) { + } }; /** @@ -79,7 +85,7 @@ private void removeCompletedJob(org.eclipse.core.runtime.jobs.Job job) { * 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 file File to scan * @param problemHelper Problem context (unused in current impl, for alignment) * @return true if scheduled, false if already pending */ @@ -90,7 +96,7 @@ public boolean scheduleInspection(IFile file, ProblemHelper problemHelper) { /** * Schedule a scan for a file with custom debounce delay. * - * @param file File to scan + * @param file File to scan * @param delayMs Debounce delay in milliseconds * @return true if scheduled, false if already pending */ @@ -121,7 +127,7 @@ public boolean scheduleInspection(IFile file, long delayMs) { scanJob.schedule(delayMs); CxLogger.info(LOG_TAG + " Scheduled scan for: " + filePath + - " (delay=" + delayMs + "ms)"); + " (delay=" + delayMs + "ms)"); return true; } catch (Exception e) { @@ -139,7 +145,7 @@ public boolean scheduleInspection(IFile file, long delayMs) { * - While typing: reschedule (cancel, start new 1s timer) * - After user pauses: job runs * - * @param file File to reschedule + * @param file File to reschedule * @param delayMs New debounce delay * @return true if rescheduled, false if no pending job */ @@ -164,7 +170,7 @@ public boolean rescheduleInspection(IFile file, long delayMs) { existingJob.reschedule(delayMs); CxLogger.info(LOG_TAG + " Rescheduled scan for: " + filePath + - " (delay=" + delayMs + "ms)"); + " (delay=" + delayMs + "ms)"); return true; } catch (Exception e) { @@ -230,6 +236,6 @@ public int getPendingScansCount() { */ public String getStatistics() { return "Pending scans: " + pendingScans.size() + - ", Tracked files: " + pendingScans.keySet(); + ", Tracked files: " + pendingScans.keySet(); } } diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java index ab89c96b..d6db2172 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemDecorator.java @@ -16,8 +16,10 @@ import org.eclipse.ui.texteditor.ITextEditor; import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; import com.checkmarx.eclipse.devassist.model.Location; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.common.utils.CxLogger; /** @@ -36,8 +38,7 @@ 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<>(); + private static final Map> fileAnnotations = new HashMap<>(); /** * Render scan results as annotations in the editor. @@ -45,7 +46,7 @@ public class ProblemDecorator { * 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 file File that was scanned * @param scanIssues Issues to visualize */ public static void decorateEditor(IFile file, List scanIssues) { @@ -56,8 +57,10 @@ public static void decorateEditor(IFile file, List scanIssues) { 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 + // **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 { @@ -70,7 +73,7 @@ public static void decorateEditor(IFile file, List scanIssues) { // Get annotation model from editor IAnnotationModel annotationModel = editor.getDocumentProvider() - .getAnnotationModel(editor.getEditorInput()); + .getAnnotationModel(editor.getEditorInput()); if (annotationModel == null) { CxLogger.warning(LOG_TAG + "No annotation model available"); @@ -91,6 +94,13 @@ public static void decorateEditor(IFile file, List scanIssues) { for (ScanIssue issue : scanIssues) { try { + // Ensure the IMarker CheckmarxMarkerResolutionGenerator's + // Ctrl+1/quick-fix-in-hover + // actions anchor to exists as soon as the squiggly does, rather than only after + // the + // user separately navigates to this finding from the Findings view. + MarkerIssueMapper.ensureMarker(file, issue); + FindingsAnnotation annotation = createAnnotation(editor, issue); if (annotation != null) { annotation.addButton(filePath, null); @@ -100,7 +110,7 @@ public static void decorateEditor(IFile file, List scanIssues) { Position pos = null; if (issue.getScanEngine() != null && - issue.getScanEngine().name().equalsIgnoreCase("OSS")) { + issue.getScanEngine().name().equalsIgnoreCase("OSS")) { // OSS: Decorate only the first line where package is declared pos = decorateOssFirstLineOnly(editor, issue); } else { @@ -114,13 +124,13 @@ public static void decorateEditor(IFile file, List scanIssues) { 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") + ")"); + (pos != null ? pos.getOffset() : "null") + ", length=" + + (pos != null ? pos.getLength() : "null") + ")"); } } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error creating annotation: " + - e.getMessage()); + e.getMessage()); e.printStackTrace(); } } @@ -129,11 +139,11 @@ public static void decorateEditor(IFile file, List scanIssues) { fileAnnotations.put(filePath, annotations); CxLogger.info(LOG_TAG + "COMPLETE: Added " + annotations.size() + - " annotations to editor"); + " annotations to editor"); } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error decorating editor: " + - e.getMessage()); + e.getMessage()); e.printStackTrace(); } } @@ -145,18 +155,18 @@ public static void decorateEditor(IFile file, List scanIssues) { * custom rendering (color, icon, tooltip) based on issue severity. * * @param editor Text editor - * @param issue Scan issue + * @param issue Scan issue * @return FindingsAnnotation, or null if creation fails */ private static FindingsAnnotation createAnnotation(ITextEditor editor, - ScanIssue issue) { + 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")); + " | Severity from issue: " + (severity != null ? severity : "NULL")); // Map severity to annotation type String annotationType = mapSeverityToAnnotationType(severity); @@ -165,57 +175,61 @@ private static FindingsAnnotation createAnnotation(ITextEditor editor, // Create annotation with issue details FindingsAnnotation annotation = new FindingsAnnotation( - annotationType, - issue.getTitle(), - issue.getDescription() - ); + annotationType, + issue.getTitle(), + issue.getDescription(), + issue); return annotation; } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error creating annotation: " + - e.getMessage()); + e.getMessage()); return null; } } - /** - * Map severity level to custom Findings annotation type. + * Map severity level to theme-aware custom Findings annotation type. + * Returns dark theme variant in dark theme, light variant in light theme. * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. * - * @param severity Severity string (MALICIOUS, CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN, OK, IGNORED) - * @return Annotation type constant (com.checkmarx.eclipse.findings.{severity}) + * @param severity Severity string (MALICIOUS, CRITICAL, HIGH, MEDIUM, LOW, + * UNKNOWN, OK, IGNORED) + * @return Annotation type constant (com.checkmarx.eclipse.findings.{severity}[_dark]) */ private static String mapSeverityToAnnotationType(String severity) { + // Append _dark suffix if dark theme is active + String themeSuffix = DevAssistUtils.isDarkTheme() ? "_dark" : ""; + if (severity == null) { - return "com.checkmarx.eclipse.findings.unknown"; + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; } String upper = severity.toUpperCase(); if (upper.contains("MALICIOUS")) { - return "com.checkmarx.eclipse.findings.malicious"; + return "com.checkmarx.eclipse.findings.malicious" + themeSuffix; } if (upper.contains("CRITICAL") || upper.contains("ERROR")) { - return "com.checkmarx.eclipse.findings.critical"; + return "com.checkmarx.eclipse.findings.critical" + themeSuffix; } if (upper.contains("HIGH")) { - return "com.checkmarx.eclipse.findings.high"; + return "com.checkmarx.eclipse.findings.high" + themeSuffix; } if (upper.contains("MEDIUM")) { - return "com.checkmarx.eclipse.findings.medium"; + return "com.checkmarx.eclipse.findings.medium" + themeSuffix; } if (upper.contains("LOW") || upper.contains("INFO")) { - return "com.checkmarx.eclipse.findings.low"; + return "com.checkmarx.eclipse.findings.low" + themeSuffix; } if (upper.contains("UNKNOWN")) { - return "com.checkmarx.eclipse.findings.unknown"; + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; } if (upper.contains("OK")) { - return "com.checkmarx.eclipse.findings.ok"; + return "com.checkmarx.eclipse.findings.ok" + themeSuffix; } if (upper.contains("IGNORED")) { - return "com.checkmarx.eclipse.findings.ignored"; + return "com.checkmarx.eclipse.findings.ignored" + themeSuffix; } - return "com.checkmarx.eclipse.findings.unknown"; + return "com.checkmarx.eclipse.findings.unknown" + themeSuffix; } /** @@ -226,8 +240,9 @@ private static String mapSeverityToAnnotationType(String severity) { * 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 + * @param issue OSS issue + * @return Position covering the entire first line, or null if unable to + * determine */ /** * Decorate the complete OSS dependency block using the first and last @@ -242,8 +257,9 @@ private static String mapSeverityToAnnotationType(String severity) { * after the last EndIndex are not decorated. * * @param editor Text editor - * @param issue OSS issue - * @return Position covering the complete OSS dependency block, or null if unable to determine + * @param issue OSS issue + * @return Position covering the complete OSS dependency block, or null if + * unable to determine */ private static Position decorateOssFirstLineOnly(ITextEditor editor, ScanIssue issue) { @@ -344,6 +360,7 @@ private static Position decorateOssFirstLineOnly(ITextEditor editor, ScanIssue i return null; } } + /** * Calculate the precise source range for an annotation. * @@ -352,108 +369,111 @@ private static Position decorateOssFirstLineOnly(ITextEditor editor, ScanIssue i * - ASCA API: Returns character positions that are LINE-RELATIVE offsets * * @param editor Text editor - * @param issue Scan issue with location info + * @param issue Scan issue with location info * @return org.eclipse.jface.text.Position representing the precise range */ - public static Position calculateRange(ITextEditor editor, ScanIssue issue) { - try { - IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); - if (document == null) return new org.eclipse.jface.text.Position(0, 1); - - int docLength = document.getLength(); - - // 1. Precise location-based offset calculation - if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { - Location location = issue.getLocations().get(0); - int rawStart = location.getStartIndex(); - int rawEnd = location.getEndIndex(); - int line = Math.max(0, location.getLine() - 1); - - IRegion lineInfo = document.getLineInformation(line); - int lineOffset = lineInfo.getOffset(); - int lineLength = lineInfo.getLength(); - - int trimIndent = getLeadingWhitespaceOffset(document, lineOffset, lineLength); - // Use explicit flag from Location instead of inferring from magnitude - boolean isAbsoluteOffset = location.isAbsoluteOffset(); - - int charStart = isAbsoluteOffset ? rawStart : (lineOffset + rawStart); - int charEnd = isAbsoluteOffset ? rawEnd : (lineOffset + rawEnd); - - // If start points to the beginning of the line, shift past leading whitespace - if (charStart <= lineOffset) { - charStart = lineOffset + trimIndent; - } - - if (charEnd <= charStart) { - charEnd = lineOffset + lineLength; - } - - // Clamp offsets safely within document bounds - charStart = Math.max(0, Math.min(charStart, docLength)); - charEnd = Math.max(charStart, Math.min(charEnd, docLength)); - - if (charEnd > charStart) { - return new org.eclipse.jface.text.Position(charStart, charEnd - charStart); - } - } - - // 2. Fallback: Highlight line content (skipping leading indentation) - int targetLine = 0; - if (issue.getProblematicLineNumber() != null) { - targetLine = issue.getProblematicLineNumber() - 1; - } else if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { - targetLine = issue.getLocations().get(0).getLine() - 1; - } - - int line = Math.max(0, Math.min(targetLine, document.getNumberOfLines() - 1)); - IRegion lineInfo = document.getLineInformation(line); - - int trimIndent = getLeadingWhitespaceOffset(document, lineInfo.getOffset(), lineInfo.getLength()); - int startOffset = lineInfo.getOffset() + trimIndent; - int length = Math.max(1, lineInfo.getLength() - trimIndent); - - return new org.eclipse.jface.text.Position(startOffset, length); - - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Error calculating range: " + e.getMessage()); - return new org.eclipse.jface.text.Position(0, 1); - } + private static Position calculateRange(ITextEditor editor, ScanIssue issue) { + try { + IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + if (document == null) + return new org.eclipse.jface.text.Position(0, 1); + + int docLength = document.getLength(); + + // 1. Precise location-based offset calculation + if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + Location location = issue.getLocations().get(0); + int rawStart = location.getStartIndex(); + int rawEnd = location.getEndIndex(); + int line = Math.max(0, location.getLine() - 1); + + IRegion lineInfo = document.getLineInformation(line); + int lineOffset = lineInfo.getOffset(); + int lineLength = lineInfo.getLength(); + + int trimIndent = getLeadingWhitespaceOffset(document, lineOffset, lineLength); + // Use explicit flag from Location instead of inferring from magnitude + boolean isAbsoluteOffset = location.isAbsoluteOffset(); + + int charStart = isAbsoluteOffset ? rawStart : (lineOffset + rawStart); + int charEnd = isAbsoluteOffset ? rawEnd : (lineOffset + rawEnd); + + // If start points to the beginning of the line, shift past leading whitespace + if (charStart <= lineOffset) { + charStart = lineOffset + trimIndent; + } + + if (charEnd <= charStart) { + charEnd = lineOffset + lineLength; + } + + // Clamp offsets safely within document bounds + charStart = Math.max(0, Math.min(charStart, docLength)); + charEnd = Math.max(charStart, Math.min(charEnd, docLength)); + + if (charEnd > charStart) { + return new org.eclipse.jface.text.Position(charStart, charEnd - charStart); + } + } + + // 2. Fallback: Highlight line content (skipping leading indentation) + int targetLine = 0; + if (issue.getProblematicLineNumber() != null) { + targetLine = issue.getProblematicLineNumber() - 1; + } else if (issue.getLocations() != null && !issue.getLocations().isEmpty()) { + targetLine = issue.getLocations().get(0).getLine() - 1; + } + + int line = Math.max(0, Math.min(targetLine, document.getNumberOfLines() - 1)); + IRegion lineInfo = document.getLineInformation(line); + + int trimIndent = getLeadingWhitespaceOffset(document, lineInfo.getOffset(), lineInfo.getLength()); + int startOffset = lineInfo.getOffset() + trimIndent; + int length = Math.max(1, lineInfo.getLength() - trimIndent); + + return new org.eclipse.jface.text.Position(startOffset, length); + + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error calculating range: " + e.getMessage()); + return new org.eclipse.jface.text.Position(0, 1); + } } + /** - * Calculates the number of leading whitespace characters (spaces/tabs) on a given line. + * Calculates the number of leading whitespace characters (spaces/tabs) on a + * given line. * - * @param document Text document + * @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; - } + 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 filePath File path * @param annotationModel Annotation model */ private static void clearAnnotations(String filePath, - IAnnotationModel annotationModel) { + IAnnotationModel annotationModel) { try { List previousAnnotations = fileAnnotations.get(filePath); @@ -464,11 +484,75 @@ private static void clearAnnotations(String filePath, fileAnnotations.remove(filePath); CxLogger.info(LOG_TAG + " Cleared " + previousAnnotations.size() + - " previous annotations"); + " previous annotations"); } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing annotations: " + - e.getMessage()); + e.getMessage()); + } + } + + /** + * Clear all annotations from all open editors (used on logout). + * Removes all FindingsAnnotation objects from the annotation models + * of currently open editors. + */ + public static void clearAllAnnotations() { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return; + } + + for (var window : workbench.getWorkbenchWindows()) { + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + continue; + } + + // Get all open editors + org.eclipse.ui.IEditorReference[] editors = page.getEditorReferences(); + for (org.eclipse.ui.IEditorReference editorRef : editors) { + try { + org.eclipse.ui.IEditorPart editorPart = editorRef.getEditor(false); + if (editorPart == null) { + continue; + } + + // Use adapter pattern to get ITextEditor + ITextEditor editor = editorPart.getAdapter(ITextEditor.class); + if (editor == null) { + continue; + } + + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + if (annotationModel == null) { + continue; + } + + // Remove all FindingsAnnotation objects + java.util.List toRemove = new java.util.ArrayList<>(); + annotationModel.getAnnotationIterator().forEachRemaining(annotation -> { + if (annotation instanceof FindingsAnnotation) { + toRemove.add(annotation); + } + }); + + for (Annotation annotation : toRemove) { + annotationModel.removeAnnotation(annotation); + } + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing annotations from editor: " + e.getMessage()); + } + } + } + + // Clear the fileAnnotations map + fileAnnotations.clear(); + CxLogger.info(LOG_TAG + " All annotations cleared from all open editors"); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Error clearing all annotations: " + e.getMessage()); } } @@ -492,7 +576,8 @@ private static ITextEditor findOpenEditor(IFile file) { // Workbench window not available, try all windows for (var window : workbench.getWorkbenchWindows()) { page = window.getActivePage(); - if (page != null) break; + if (page != null) + break; } } @@ -505,7 +590,7 @@ private static ITextEditor findOpenEditor(IFile file) { Object input = editor.getEditorInput(); if (input instanceof org.eclipse.ui.IFileEditorInput) { IFile editorFile = ((org.eclipse.ui.IFileEditorInput) input) - .getFile(); + .getFile(); if (editorFile.equals(file)) { // Try method 1: Direct ITextEditor instance if (editor instanceof ITextEditor) { @@ -522,7 +607,7 @@ private static ITextEditor findOpenEditor(IFile file) { } } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error finding open editor: " + - e.getMessage()); + e.getMessage()); } return null; @@ -540,7 +625,8 @@ private static ITextEditor findOpenEditor(IFile file) { */ public static void clearDecorations(IFile file) { try { - // **FIX: Use getLocation() (absolute path) for consistency with decorateEditor()** + // **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); @@ -552,7 +638,7 @@ public static void clearDecorations(IFile file) { } IAnnotationModel annotationModel = editor.getDocumentProvider() - .getAnnotationModel(editor.getEditorInput()); + .getAnnotationModel(editor.getEditorInput()); if (annotationModel != null) { clearAnnotations(filePath, annotationModel); @@ -562,7 +648,7 @@ public static void clearDecorations(IFile file) { } catch (Exception e) { CxLogger.warning(LOG_TAG + " Error clearing decorations: " + - e.getMessage()); + e.getMessage()); } } @@ -573,10 +659,10 @@ public static void clearDecorations(IFile file) { */ public static String getStatistics() { int totalAnnotations = fileAnnotations.values().stream() - .mapToInt(List::size) - .sum(); + .mapToInt(List::size) + .sum(); return "Decorated files: " + fileAnnotations.size() + - ", Total annotations: " + totalAnnotations; + ", Total annotations: " + totalAnnotations; } /** @@ -585,16 +671,17 @@ public static String getStatistics() { * Delegates to the decorateEditor() path which handles annotation creation * and display in the editor's gutter and line highlighting. * - * @param problemHelper Problem helper with context (used to locate the file being edited) - * @param scanIssue Scan issue to highlight - * @param isProblem Whether this is a problem (not just note) + * @param problemHelper Problem helper with context (used to locate the file + * being edited) + * @param scanIssue Scan issue to highlight + * @param isProblem Whether this is a problem (not just note) * @param problemLineNumber Line number to highlight */ public void highlightLineAddGutterIconForProblem( - ProblemHelper problemHelper, - ScanIssue scanIssue, - boolean isProblem, - int problemLineNumber) { + ProblemHelper problemHelper, + ScanIssue scanIssue, + boolean isProblem, + int problemLineNumber) { if (!isProblem || scanIssue == null) { return; @@ -620,7 +707,8 @@ public void highlightLineAddGutterIconForProblem( * 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) + * @param project Project to clear (used for context, actual clearing is + * project-wide) */ public static void removeAllHighlighters(org.eclipse.core.resources.IProject project) { try { @@ -636,8 +724,8 @@ public static void removeAllHighlighters(org.eclipse.core.resources.IProject pro try { ITextEditor editor = (ITextEditor) ref.getEditor(false); if (editor != null) { - IAnnotationModel annotationModel = - editor.getDocumentProvider().getAnnotationModel(editor.getEditorInput()); + IAnnotationModel annotationModel = editor.getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); if (annotationModel != null) { for (List annotations : fileAnnotations.values()) { for (Annotation ann : annotations) { @@ -665,4 +753,3 @@ public static void removeAllHighlighters(org.eclipse.core.resources.IProject pro } } } - diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java index 037c732d..9aedecc4 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/problems/ProblemHolderService.java @@ -218,11 +218,13 @@ public List removeAllIssuesForScanner(String scannerType) { } /** - * Clear all caches (on project close). + * Clear all caches (on project close or logout). */ public void clearAll() { fileToScanIssues.clear(); + fileToProblemDescriptors.clear(); CxLogger.info(LOG_TAG + " All caches cleared"); + publishIssuesUpdated(); } /** diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationLinkHandler.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationLinkHandler.java new file mode 100644 index 00000000..eb731f7a --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationLinkHandler.java @@ -0,0 +1,321 @@ +package com.checkmarx.eclipse.devassist.remediation; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; + +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.SEPERATOR; +import static java.lang.String.format; + +/** + * Handler for remediation actions triggered from tooltips in the editor. + * This class processes remediation links extracted from hover popups and executes + * the corresponding actions such as fixing issues, viewing details, or ignoring + * certain types of issues. + * + * Adapted from JetBrains IntelliJ implementation to work with Eclipse's link + * handling mechanism via browser LocationListener. + */ +public class RemediationLinkHandler { + + private static final String FIX = "copyfixprompt"; + private static final String VIEW_DETAILS = "viewdetails"; + private static final String IGNORE_THIS_TYPE = "ignorethis"; + private static final String IGNORE_ALL_OF_THIS_TYPE = "ignoreallofthis"; + + private final RemediationManager remediationManager = new RemediationManager(); + + /** + * Handles a remediation link with a provided scan issue context. + * This is the primary entry point when a link is clicked in the hover popup. + * + * @param link the link string containing action and issue information, + * formatted as: action|issueId|engineName + * @param scanIssue the scan issue context for the remediation action + * @return true if the link was handled successfully, false otherwise. + */ + public boolean handleLink(@NonNull String link, @NonNull ScanIssue scanIssue) { + if (!link.contains(SEPERATOR)) { + CxLogger.warning("RTS-Fix: Remediation action failed, Link is not valid: " + link); + return false; + } + + String[] linkData = link.split(SEPERATOR); + String scanIssueId = extractIssueId(linkData); + if (scanIssueId.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan issue id not found in remediation link: " + link); + return false; + } + + String action = extractAction(linkData); + if (action.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Action not found in remediation link: " + link); + return false; + } + + String engineName = extractEngineName(linkData); + if (Objects.isNull(engineName) || engineName.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan engine name not found in remediation link: " + link); + return false; + } + + CxLogger.info(format("RTS-Fix: %s Remediation action called for engine: %s with issue id: %s", action, engineName, scanIssueId)); + + return handleActions(action, scanIssue, scanIssueId); + } + + /** + * Handles a remediation link by extracting action information and searching for the issue. + * This is an alternative entry point when the scan issue is not readily available + * and must be retrieved from the ProblemHolderService. + * + * @param link the link string containing action and issue information, + * formatted as: action|issueId|engineName + * @return true if the link was handled successfully, false otherwise. + */ + public boolean handleLink(@NonNull String link) { + if (!link.contains(SEPERATOR)) { + CxLogger.warning("RTS-Fix: Remediation action failed, Link is not valid: " + link); + return false; + } + + String[] linkData = link.split(SEPERATOR); + String scanIssueId = extractIssueId(linkData); + if (scanIssueId.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan issue id not found in remediation link: " + link); + return false; + } + + String action = extractAction(linkData); + if (action.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Action not found in remediation link: " + link); + return false; + } + + String engineName = extractEngineName(linkData); + if (Objects.isNull(engineName) || engineName.isEmpty()) { + CxLogger.warning("RTS-Fix: Remediation action failed, Scan engine name not found in remediation link: " + link); + return false; + } + + CxLogger.info(format("RTS-Fix: %s Remediation action called for engine: %s with issue id: %s", action, engineName, scanIssueId)); + + ScanIssue scanIssue = getScanIssue(scanIssueId, engineName); + if (Objects.isNull(scanIssue)) { + CxLogger.warning(format("RTS-Fix: %s Remediation action failed. Scan issue is not found for the given issue-id: %s", action, scanIssueId)); + return false; + } + + return handleActions(action, scanIssue, scanIssueId); + } + + /** + * Handles specific remediation actions for a given scan issue. + * Depending on the provided action link, it performs appropriate actions + * such as applying a fix, viewing issue details, or ignoring the issue type. + * + * @param action the remediation action to be performed + * @param scanIssue the scan issue on which the action is performed + * @param actionId the action ID for vulnerability-specific fixes + * @return true if the action is successfully handled, false otherwise + */ + private boolean handleActions(@NonNull String action, @NonNull ScanIssue scanIssue, @NonNull String actionId) { + // Note: Commented code sections below are preserved as requested for future use + // when dependencies become available (IgnoreManager, TelemetryService) + + switch (action) { + case FIX: +// TelemetryService.logFixWithCxOneAssistAction(scanIssue); + remediationManager.fixWithCxOneAssist(scanIssue, actionId); + break; + case VIEW_DETAILS: +// TelemetryService.logViewDetailsAction(scanIssue); + remediationManager.viewDetails(scanIssue, actionId); + break; + case IGNORE_THIS_TYPE: +// IgnoreManager ignoremanager = IgnoreManager.getInstance(project); +// ignoremanager.addIgnoredEntry(scanIssue, actionId); +// TelemetryService.logIgnorePackageAction(scanIssue); + CxLogger.warning("RTS-Fix: IGNORE_THIS_TYPE action not yet implemented"); + break; + case IGNORE_ALL_OF_THIS_TYPE: +// IgnoreManager ignoremanager = IgnoreManager.getInstance(project); +// ignoremanager.addAllIgnoredEntry(scanIssue, actionId); +// TelemetryService.logIgnoreAllAction(scanIssue); + CxLogger.warning("RTS-Fix: IGNORE_ALL_OF_THIS_TYPE action not yet implemented"); + break; + default: + CxLogger.warning(format("RTS-Fix: Remediation action %s is not supported", action)); + return false; + } + return true; + } + + /** + * Extracts the engine name from the link data array. + * + * @param linkData split link data array + * @return scan engine name, or empty string if not found + */ + private String extractEngineName(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 2 ? linkData[2] : ""; + } + + /** + * Extracts the issue id from the link data array. + * + * @param linkData split link data array + * @return scan issue id, or empty string if not found + */ + private String extractIssueId(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 1 ? linkData[1] : ""; + } + + /** + * Extracts the action from the link data array. + * + * @param linkData split link data array + * @return remediation action, or empty string if not found + */ + private String extractAction(String[] linkData) { + return Objects.nonNull(linkData) && linkData.length > 0 ? linkData[0] : ""; + } + + /** + * Retrieves a specific scan issue based on the provided issue ID and engine name. + * Queries the ProblemHolderService to find the matching issue by either scan issue ID + * or vulnerability ID. This method iterates through all cached scan issues to find + * a match. + * + * @param issueId the unique identifier of the scan issue to retrieve + * @param engineName the scan engine name to match + * @return the {@link ScanIssue} matching the given issue ID and engine, or null if not found + */ + @Nullable + private ScanIssue getScanIssue(@NonNull String issueId, @NonNull String engineName) { + try { + CxLogger.warning("RTS-Fix: Searching for scan issue with ID: " + issueId + ", engine: " + engineName); + + // Get all cached scan issues from all files + java.util.Map> allIssuesMap = getAllCachedScanIssues(); + if (allIssuesMap == null || allIssuesMap.isEmpty()) { + CxLogger.warning("RTS-Fix: No scan issues found in cache to handle the link"); + return null; + } + + // Flatten the map into a single list of all issues + List allIssues = new java.util.ArrayList<>(); + for (List issueList : allIssuesMap.values()) { + allIssues.addAll(issueList); + } + + ScanIssue scanIssue = getScanIssueUsingScanIssueId(allIssues, issueId, engineName); + if (Objects.isNull(scanIssue)) { + return getScanIssueUsingVulnerabilityId(allIssues, issueId, engineName); + } + return scanIssue; + } catch (Exception exception) { + CxLogger.warning("RTS-Fix: Exception occurred while retrieving scan issue"); + return null; + } + } + + /** + * Helper method to get all cached scan issues from the workspace. + * Since ProblemHolderService is project-scoped, we try to find a cached instance + * or return an empty map if none are available. + * + * @return Map of all cached scan issues (file path → list of issues) + */ + @Nullable + private java.util.Map> getAllCachedScanIssues() { + try { + // Try to get issues from Eclipse workspace root (project-agnostic approach) + // In a multi-project workspace, this will only get issues from the currently + // active project's ProblemHolderService instance. For a complete solution, + // iterate through all open projects (requires org.eclipse.core.resources.IWorkspace) + org.eclipse.core.resources.IWorkspaceRoot root = + org.eclipse.core.resources.ResourcesPlugin.getWorkspace().getRoot(); + org.eclipse.core.resources.IProject[] projects = root.getProjects(); + + java.util.Map> combinedIssues = new java.util.HashMap<>(); + + for (org.eclipse.core.resources.IProject project : projects) { + try { + if (project.isOpen()) { + ProblemHolderService service = ProblemHolderService.getInstance(project); + if (service != null) { + java.util.Map> projectIssues = service.getAllScanIssues(); + if (projectIssues != null) { + combinedIssues.putAll(projectIssues); + } + } + } + } catch (Exception e) { + // Skip projects with errors, continue with others + CxLogger.warning("RTS-Fix: Error accessing project " + project.getName()); + } + } + + return combinedIssues; + } catch (Exception e) { + CxLogger.warning("RTS-Fix: Error retrieving cached scan issues from workspace"); + return null; + } + } + + /** + * Retrieves the ScanIssue corresponding to the given scan issue ID from the provided list. + * Matches both the issue ID and the scan engine name. + * + * @param scanIssueList list of scan issues to search + * @param issueId scan issue id to match + * @param engineName scan engine name to match + * @return the ScanIssue matching the given issueId and engine, or null if no match is found + */ + @Nullable + private ScanIssue getScanIssueUsingScanIssueId(@NonNull List scanIssueList, + @NonNull String issueId, + @NonNull String engineName) { + return scanIssueList.stream() + .filter(issue -> Objects.nonNull(issue) + && issue.getScanIssueId().equals(issueId) + && issue.getScanEngine().name().equalsIgnoreCase(engineName)) + .findFirst() + .orElse(null); + } + + /** + * Retrieves the ScanIssue by searching through vulnerabilities of scan issues. + * Used when a vulnerability ID is provided instead of a scan issue ID. + * Matches both the vulnerability ID and the scan engine name. + * + * @param scanIssueList list of scan issues to search + * @param issueId vulnerability id to match + * @param engineName scan engine name to match + * @return the ScanIssue containing the matching vulnerability, or null if not found + */ + @Nullable + private ScanIssue getScanIssueUsingVulnerabilityId(@NonNull List scanIssueList, + @NonNull String issueId, + @NonNull String engineName) { + for (ScanIssue scanIssue : scanIssueList) { + if (Objects.nonNull(scanIssue) && scanIssue.getScanEngine().name().equalsIgnoreCase(engineName) + && Objects.nonNull(scanIssue.getVulnerabilities()) && !scanIssue.getVulnerabilities().isEmpty()) { + for (var vulnerability : scanIssue.getVulnerabilities()) { + if (vulnerability.getVulnerabilityId().equals(issueId)) { + return scanIssue; + } + } + } + } + return null; + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java index 40f6798a..05484497 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java @@ -8,6 +8,8 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.swt.widgets.Display; +import org.slf4j.Logger; + import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.model.ScanIssue; @@ -30,7 +32,8 @@ */ public final class RemediationManager { -// private static final Logger LOGGER = PluginUtils.getLogger(RemediationManager.class); + // private static final Logger LOGGER = + // PluginUtils.getLogger(RemediationManager.class); private static final String DEV_ASSIST_COPY_FIX_PROMPT = "Fix prompt copied to clipboard! Paste the prompt into Copilot chat (Agent Mode)"; @@ -58,18 +61,18 @@ public void fixWithCxOneAssist(@NonNull ScanIssue scanIssue, String actionId) { @Nullable private String buildRemediationPrompt(@NonNull ScanIssue scanIssue, String actionId) { switch (scanIssue.getScanEngine()) { - case OSS: - return buildOSSRemediationPrompt(scanIssue); - case SECRETS: - return buildSecretRemediationPrompt(scanIssue); - case CONTAINERS: - return buildContainerRemediationPrompt(scanIssue); - case IAC: - return buildIACRemediationPrompt(scanIssue, actionId); - case ASCA: - return buildASCARemediationPrompt(scanIssue, actionId); - default: - return null; + case OSS: + return buildOSSRemediationPrompt(scanIssue); + case SECRETS: + return buildSecretRemediationPrompt(scanIssue); + case CONTAINERS: + return buildContainerRemediationPrompt(scanIssue); + case IAC: + return buildIACRemediationPrompt(scanIssue, actionId); + case ASCA: + return buildASCARemediationPrompt(scanIssue, actionId); + default: + return null; } } @@ -98,7 +101,7 @@ private void applyFix(@NonNull ScanIssue scanIssue, @Nullable String prompt) { scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } else { // Fallback: Copy to clipboard with notification when Copilot is not available - if (copyToClipboardAndNotify(prompt,notificationTitle, DEV_ASSIST_COPY_FIX_PROMPT)) { + if (copyToClipboardAndNotify(prompt, notificationTitle, DEV_ASSIST_COPY_FIX_PROMPT)) { CxLogger.info(format("RTS-Fix: %s remediation completed (clipboard) for issue: %s, for file: %s", scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } @@ -154,18 +157,18 @@ public void viewDetails(@NonNull ScanIssue scanIssue, String actionId) { @Nullable private String buildExplanationPrompt(@NonNull ScanIssue scanIssue, String actionId) { switch (scanIssue.getScanEngine()) { - case OSS: - return buildOSSExplanationPrompt(scanIssue); - case SECRETS: - return buildSecretExplanationPrompt(scanIssue); - case CONTAINERS: - return buildContainerExplanationPrompt(scanIssue); - case IAC: - return buildIACExplanationPrompt(scanIssue, actionId); - case ASCA: - return buildASCAExplanationPrompt(scanIssue, actionId); - default: - return null; + case OSS: + return buildOSSExplanationPrompt(scanIssue); + case SECRETS: + return buildSecretExplanationPrompt(scanIssue); + case CONTAINERS: + return buildContainerExplanationPrompt(scanIssue); + case IAC: + return buildIACExplanationPrompt(scanIssue, actionId); + case ASCA: + return buildASCAExplanationPrompt(scanIssue, actionId); + default: + return null; } } @@ -195,8 +198,9 @@ private void applyViewDetails(@NonNull ScanIssue scanIssue, @Nullable String pro } else { // Fallback: Copy to clipboard with notification when Copilot is not available if (copyToClipboardAndNotify(prompt, notificationTitle, DEV_ASSIST_COPY_VIEW_DETAILS_PROMPT)) { - CxLogger.info(format("RTS-ViewDetails: %s explanation completed (clipboard) for issue: %s, for file: %s", - scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + CxLogger.info( + format("RTS-ViewDetails: %s explanation completed (clipboard) for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } } } @@ -366,7 +370,7 @@ private String buildASCAExplanationPrompt(ScanIssue scanIssue, String actionId) private String getNotificationTitle(ScanEngine scanEngine) { return DevAssistUtils.getAgentName() + " - " + scanEngine.name(); } - + /** * Copies the prompt to the clipboard and shows a balloon notification * confirming it. @@ -383,7 +387,8 @@ private static boolean copyToClipboardAndNotify(String prompt, String notifyTitl popup.open(); }); } else { - CxLogger.error("RTS-Fix: Failed to copy prompt to clipboard", new Exception("RTS-Fix: Failed to copy prompt to clipboard")); + CxLogger.error("RTS-Fix: Failed to copy prompt to clipboard", + new Exception("RTS-Fix: Failed to copy prompt to clipboard")); } return copied; } diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java index 295e3bb3..c3173e68 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScanResultAdaptor.java @@ -6,15 +6,18 @@ import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.common.utils.CxLogger; import java.util.*; import java.util.stream.Collectors; /** - * Adapter class for handling ASCA scan results and converting them into a standardized format. + * 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 + * 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: @@ -29,17 +32,17 @@ 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. + * Constructs an instance of AscaScanResultAdaptor with the specified ASCA scan + * results. * * @param ascaScanResult the ASCA scan results to be wrapped - * @param filePath the path of the file being scanned + * @param filePath the path of the file being scanned */ public AscaScanResultAdaptor(com.checkmarx.ast.asca.ScanResult ascaScanResult, String filePath) { this.ascaScanResult = ascaScanResult; @@ -78,11 +81,10 @@ private List buildIssues() { .collect(Collectors.groupingBy( ScanDetail::getLine, Collectors.collectingAndThen(Collectors.toList(), detailsList -> { - detailsList.sort(Comparator.comparingInt(detail -> - getSeverityPrecedence(detail.getSeverity()))); + detailsList.sort( + Comparator.comparingInt(detail -> getSeverityPrecedence(detail.getSeverity()))); return detailsList; - }) - )); + }))); List issues = groupedIssues.values().stream() .map(this::createScanIssueForGroup) @@ -94,10 +96,13 @@ private List buildIssues() { } /** - * Creates a ScanIssue from a group of ASCA scan details that are on the same line. + * 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 + * @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()) { @@ -129,9 +134,11 @@ private ScanIssue createScanIssueForGroup(List ascaScanDetails) { } /** - * Creates a ScanIssue with appropriate title and basic properties from a group of ASCA scan details. + * 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) + * @param ascaScanDetails the list of ASCA scan details (already sorted by + * severity) * @return a ScanIssue with basic properties set */ private ScanIssue getScanIssue(List ascaScanDetails) { @@ -141,7 +148,7 @@ private ScanIssue getScanIssue(List ascaScanDetails) { // 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; + title = ascaScanDetails.size() + DevAssistConstants.MULTIPLE_ASCA_ISSUES; } else { title = firstDetail.getRuleName(); } @@ -189,14 +196,15 @@ private Vulnerability createVulnerability(ScanDetail scanDetail, String override } /** - * Updates the ScanIssue title and location based on vulnerability count and scan details. + * 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); + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + DevAssistConstants.MULTIPLE_ASCA_ISSUES); } // Add location information from first detail diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java index 7cd51fa5..a6fecf7a 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -77,7 +77,8 @@ public com.checkmarx.eclipse.devassist.common.ScanResult scan(String /** * Primary scan method - gets file content and executes scan. */ - public com.checkmarx.eclipse.devassist.common.ScanResult scanWithDocument(String filePath, IDocument document) { + public com.checkmarx.eclipse.devassist.common.ScanResult scanWithDocument(String filePath, + IDocument document) { return scanInternal(filePath, document, project); } @@ -86,7 +87,8 @@ public void close() throws Exception { // No resources to close } - private com.checkmarx.eclipse.devassist.common.ScanResult scanInternal(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; } @@ -203,7 +205,8 @@ private Object executeAscaScanner(String filePath, String ignoreFilePath) { /** * Get ignore file path for ASCA scanning. - * Returns empty string by default - can be extended to read from .checkmarxIgnored file. + * Returns empty string by default - can be extended to read from + * .checkmarxIgnored file. */ private String getIgnoreFilePath() { return ""; @@ -266,7 +269,6 @@ private String saveTempFile(String fileName, String fileContent) { } } - /** * Sanitize file name to prevent directory traversal attacks. */ @@ -330,7 +332,6 @@ private void deleteFile(String filePath) { } } - 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; diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index 2c238a43..1cece719 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -6,6 +6,7 @@ import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.common.utils.CxLogger; @@ -26,252 +27,280 @@ * 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. + * folder management, + * and direct invocation of Checkmarx Container Realtime scanning via + * CxWrapperFactory. */ public class ContainerScannerService extends BaseScannerService { - private static final String LOG_TAG = "[CONTAINER-SERVICE]"; - private static final String CONTAINER_DIR = "CxContainer"; - private static final Object SCAN_LOCK = new Object(); - - private static final List CONTAINERS_FILE_PATTERNS = List.of("**/dockerfile*", "**/*.containerfile", - "**/*.image", "**/docker-compose*.yml", "**/docker-compose*.yaml"); - - private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of("chart.yaml", "chart.yml", "values.yaml", - "values.yml"); - - private String fileType; - - public ContainerScannerService(IProject project) { - super(project, createConfig()); - } - - /** - * Create default Container scanner configuration. - */ - public static ScannerConfig createConfig() { - return ScannerConfig.builder().engineName(ScanEngine.CONTAINERS.name()) - .configSection(DevAssistConstants.CONTAINER_REALTIME_SCANNER) - .activateKey(DevAssistConstants.ACTIVATE_CONTAINER_REALTIME_SCANNER) - .enabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_START) - .disabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_DISABLED) - .errorMessage(DevAssistConstants.ERROR_CONTAINER_REALTIME_SCANNER).build(); - } - - @Override - protected boolean isFileTypeSupported(String filePath) { - return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); - } - - /** - * Checks whether the supplied file path matches container file patterns - * (Dockerfile, Docker Compose, etc.). - */ - private boolean isContainersFilePatternMatching(String filePath) { - String lowerPath = filePath.toLowerCase(); - List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() - .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)).collect(Collectors.toList()); - - Path path = Paths.get(lowerPath); - for (PathMatcher pathMatcher : pathMatchers) { - if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { - if (DevAssistUtils.isDockerComposeFile(lowerPath)) { - this.fileType = DevAssistUtils.DOCKER_COMPOSE; - } else if (DevAssistUtils.isDockerFile(lowerPath)) { - this.fileType = DevAssistUtils.DOCKERFILE; - } - return true; - } - } - return false; - } - - /** - * Checks whether the supplied file path is part of a Helm chart. - */ - public boolean isHelmFile(String filePath) { - if (filePath == null) { - return false; - } - String lowerPath = filePath.toLowerCase(); - if (DevAssistUtils.isYamlFile(lowerPath)) { - String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); - if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { - return false; - } - if (lowerPath.contains("/helm/")) { - this.fileType = DevAssistUtils.HELM; - return true; - } - } - return false; - } - - /** - * Primary scan method. Reads content, creates isolated temporary directory - * structure, executes the container realtime scan, and updates ignored issues. - */ - public ScanResult scan(String filePath, IDocument document, IProject proj) { - if (!shouldScanFile(filePath)) { - return null; - } - - synchronized (SCAN_LOCK) { - String fileContent = getFileContent(filePath, document); - if (fileContent == null || fileContent.isBlank()) { - CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); - return null; - } - - Path tempBaseDir = getSecureTempDirectory(); - Path tempSubFolder = null; - Path tempFilePath = null; - - try { - String fileName = Paths.get(filePath).getFileName().toString(); - String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; - String folderName = prefix + generateFileHash(filePath); - tempSubFolder = tempBaseDir.resolve(folderName).normalize(); - createTempFolder(tempSubFolder); - tempFilePath = tempSubFolder.resolve(fileName).normalize(); - Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); - CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); -// String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj != null ? proj : this.project); - ContainersRealtimeResults scanResults = null; - try { - scanResults = CxWrapperFactory.build().containersRealtimeScan(tempFilePath.toString(), ""); - } catch (Exception e) { - CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); - } - updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, - filePath); - return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); - } catch (IOException e) { - CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); - } finally { - if (Objects.nonNull(tempSubFolder)) { - deleteTempFolder(tempSubFolder); - } - } - } - return null; - } - - /** - * Re-runs scan without ignore settings to calculate line updates for ignored - * entries. - */ - private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { -// try { -// IgnoreManager ignoreManager = new IgnoreManager(proj); -// if (ignoreManager.hasIgnoredEntries(ScanEngine.CONTAINERS)) { -// CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for ignored packages"); -// ContainersRealtimeResults fullScanResults = CxWrapperFactory.build() -// .containersRealtimeScan(tempFilePath, ""); -// -// if (fullScanResults != null) { -// ContainerScanResultAdaptor fullScanResultAdaptor = new ContainerScanResultAdaptor(fullScanResults, this.fileType, filePath); -// ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, filePath); -// } -// } -// } catch (Exception e) { -// CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored file line numbers: " + e.getMessage()); -// } - } - - /** - * Reads file content from Eclipse IDocument buffer or disk filesystem. - */ - private String getFileContent(String filePath, IDocument document) { - if (document != null) { - String content = document.get(); - if (content != null && !content.isEmpty()) { - return content; - } - } - - if (filePath == null || filePath.isBlank()) { - return null; - } - - try { - Path nioPath = Paths.get(filePath); - if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { - return Files.readString(nioPath, StandardCharsets.UTF_8); - } - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); - } - return null; - } - - /** - * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary - * directory names. - */ - private String generateFileHash(String relativePath) { - try { - LocalTime time = LocalTime.now(); - String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); - String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); - StringBuilder hexString = new StringBuilder(); - for (byte b : hashBytes) { - hexString.append(String.format("%02x", b)); - } - return hexString.substring(0, 16); - } catch (NoSuchAlgorithmException e) { - return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); - } - } - - private Path getSecureTempDirectory() { - String tempOSPath = System.getProperty("java.io.tmpdir"); - if (tempOSPath == null || tempOSPath.isBlank()) { - tempOSPath = System.getProperty("user.home"); - } - Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); - return baseTempDir.resolve(CONTAINER_DIR).normalize(); - } - - protected void createTempFolder(Path tempDir) { - if (!Files.exists(tempDir)) { - try { - Files.createDirectories(tempDir); - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); - } - } - } - - protected void deleteTempFolder(Path path) { - if (path == null || !Files.exists(path)) { - return; - } - try { - Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - CxLogger.info(LOG_TAG + " Temporary folder deleted: " + path.toAbsolutePath()); - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Failed to delete temporary directory: " + e.getMessage()); - } - } - - /** - * Compatibility method matching ScannerService interface. - */ - @Override - public ScanResult scan(String filePath) { - if (!shouldScanFile(filePath)) { - return null; - } - IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); - return scan(filePath, liveDocument, project); - } - - @Override - public void close() throws Exception { - // No persistent connections to close - } + private static final String LOG_TAG = "[CONTAINER-SERVICE]"; + private static final String CONTAINER_DIR = "CxContainer"; + private static final Object SCAN_LOCK = new Object(); + + private static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile*", + "**/*.containerfile", + "**/*.image", + "**/docker-compose*.yml", + "**/docker-compose*.yaml"); + + private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of( + "chart.yaml", + "chart.yml", + "values.yaml", + "values.yml"); + + private String fileType; + + public ContainerScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default Container scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.CONTAINERS.name()) + .configSection(DevAssistConstants.CONTAINER_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_CONTAINER_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_CONTAINER_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); + } + + /** + * Checks whether the supplied file path matches container file patterns + * (Dockerfile, Docker Compose, etc.). + */ + private boolean isContainersFilePatternMatching(String filePath) { + String lowerPath = filePath.toLowerCase(); + List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { + if (DevAssistUtils.isDockerComposeFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKER_COMPOSE; + } else if (DevAssistUtils.isDockerFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKERFILE; + } + return true; + } + } + return false; + } + + /** + * Checks whether the supplied file path is part of a Helm chart. + */ + public boolean isHelmFile(String filePath) { + if (filePath == null) { + return false; + } + String lowerPath = filePath.toLowerCase(); + if (DevAssistUtils.isYamlFile(lowerPath)) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { + return false; + } + if (lowerPath.contains("/helm/")) { + this.fileType = DevAssistUtils.HELM; + return true; + } + } + return false; + } + + /** + * Primary scan method. Reads content, creates isolated temporary directory + * structure, + * executes the container realtime scan, and updates ignored issues. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + synchronized (SCAN_LOCK) { + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); + return null; + } + + Path tempBaseDir = getSecureTempDirectory(); + Path tempSubFolder = null; + Path tempFilePath = null; + + try { + String fileName = Paths.get(filePath).getFileName().toString(); + String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; + String folderName = prefix + generateFileHash(filePath); + + tempSubFolder = tempBaseDir.resolve(folderName).normalize(); + createTempFolder(tempSubFolder); + + tempFilePath = tempSubFolder.resolve(fileName).normalize(); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + + CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); + // String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj != null ? proj + // : this.project); + + ContainersRealtimeResults scanResults = null; + try { + scanResults = CxWrapperFactory.build().containersRealtimeScan(tempFilePath.toString(), ""); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, + filePath); + + return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); + + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); + } finally { + if (Objects.nonNull(tempSubFolder)) { + deleteTempFolder(tempSubFolder); + } + } + } + return null; + } + + /** + * Re-runs scan without ignore settings to calculate line updates for ignored + * entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { + // try { + // IgnoreManager ignoreManager = new IgnoreManager(proj); + // if (ignoreManager.hasIgnoredEntries(ScanEngine.CONTAINERS)) { + // CxLogger.info(LOG_TAG + " Performing full scan to update line numbers for + // ignored packages"); + // ContainersRealtimeResults fullScanResults = CxWrapperFactory.build() + // .containersRealtimeScan(tempFilePath, ""); + // + // if (fullScanResults != null) { + // ContainerScanResultAdaptor fullScanResultAdaptor = new + // ContainerScanResultAdaptor(fullScanResults, this.fileType, filePath); + // ignoreManager.updateLineNumbersForIgnoredEntries(fullScanResultAdaptor, + // filePath); + // } + // } + // } catch (Exception e) { + // CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored file + // line numbers: " + e.getMessage()); + // } + } + + /** + * Reads file content from Eclipse IDocument buffer or disk filesystem. + */ + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + /** + * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary + * directory names. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.isBlank()) { + tempOSPath = System.getProperty("user.home"); + } + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + return baseTempDir.resolve(CONTAINER_DIR).normalize(); + } + + protected void createTempFolder(Path tempDir) { + if (!Files.exists(tempDir)) { + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } + } + } + + protected void deleteTempFolder(Path path) { + if (path == null || !Files.exists(path)) { + return; + } + try { + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + CxLogger.info(LOG_TAG + " Temporary folder deleted: " + path.toAbsolutePath()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to delete temporary directory: " + e.getMessage()); + } + } + + /** + * Compatibility method matching ScannerService interface. + */ + @Override + public ScanResult scan(String filePath) { + if (!shouldScanFile(filePath)) { + return null; + } + IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument, project); + } + + @Override + public void close() throws Exception { + // No persistent connections to close + } } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java index c1d9f48c..3a6ec5fc 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScanResultAdaptor.java @@ -7,15 +7,18 @@ import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.common.utils.CxLogger; import java.util.*; import java.util.stream.Collectors; /** - * Adapter class for handling IaC scan results and converting them into a standardized format. + * 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 + * 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: @@ -29,7 +32,6 @@ 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; @@ -73,11 +75,10 @@ private List buildIssues() { return 1; }, Collectors.collectingAndThen(Collectors.toList(), issuesList -> { - issuesList.sort(Comparator.comparingInt(issue -> - getSeverityPrecedence(issue.getSeverity()))); + issuesList + .sort(Comparator.comparingInt(issue -> getSeverityPrecedence(issue.getSeverity()))); return issuesList; - }) - )); + }))); List scanIssues = groupedIssues.values().stream() .map(this::createScanIssueForGroup) @@ -129,7 +130,7 @@ private ScanIssue getScanIssue(List iacIssues) { // 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; + title = iacIssues.size() + DevAssistConstants.MULTIPLE_IAC_ISSUES; } else { title = firstIssue.getTitle(); } @@ -176,7 +177,7 @@ private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List 1) { - scanIssue.setTitle(scanIssue.getVulnerabilities().size() + MULTIPLE_ISSUES_SUFFIX); + scanIssue.setTitle(scanIssue.getVulnerabilities().size() + DevAssistConstants.MULTIPLE_IAC_ISSUES); } // Add location information from issues @@ -184,7 +185,7 @@ private void updateScanIssueTitleAndLocation(ScanIssue scanIssue, List { private final List scanIssues; /** - * Constructs an instance of {@code OssScanResultAdaptor} with the specified OSS real-time results. + * 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 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) { @@ -49,7 +53,8 @@ public OssScanResultAdaptor(OssRealtimeResults ossRealtimeResults, String filePa /** * Retrieves the raw OSS real-time scan results wrapped by this adapter. * - * @return an {@link OssRealtimeResults} instance containing the results of the OSS scan + * @return an {@link OssRealtimeResults} instance containing the results of the + * OSS scan */ @Override public OssRealtimeResults getResults() { @@ -59,7 +64,8 @@ public OssRealtimeResults getResults() { /** * 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 + * @return a list of {@link ScanIssue} objects representing findings, or an + * empty list if none */ @Override public List getIssues() { @@ -68,7 +74,8 @@ public List getIssues() { /** * Builds a list of ScanIssue objects from the OSS scan results. - * Processes packages obtained from scan results into standardized ScanIssue items. + * Processes packages obtained from scan results into standardized ScanIssue + * items. * * @return a list of ScanIssue objects */ @@ -82,6 +89,9 @@ private List buildIssues() { List issues = packages.stream() .map(this::createScanIssue) .filter(Objects::nonNull) + .collect(Collectors.toMap(ScanIssue::getScanIssueId, java.util.function.Function.identity(), + (first, duplicate) -> first, java.util.LinkedHashMap::new)) + .values().stream() .collect(Collectors.toList()); CxLogger.info(LOG_TAG + " Converted " + issues.size() + " OSS scan issues for file: " + filePath); @@ -89,7 +99,8 @@ private List buildIssues() { } /** - * Creates a {@link ScanIssue} object based on the provided {@link OssRealtimeScanPackage}. + * 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 @@ -111,14 +122,13 @@ private ScanIssue createScanIssue(OssRealtimeScanPackage packageObj) { // Process location information if (Objects.nonNull(packageObj.getLocations()) && !packageObj.getLocations().isEmpty()) { - packageObj.getLocations().forEach(location -> - scanIssue.getLocations().add(createLocation(location))); + 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))); + packageObj.getVulnerabilities().forEach( + vulnerability -> scanIssue.getVulnerabilities().add(createVulnerability(vulnerability))); } // Set primary problem line based on first location (if available) @@ -139,7 +149,8 @@ private ScanIssue createScanIssue(OssRealtimeScanPackage packageObj) { } /** - * Creates a {@link Vulnerability} instance based on the provided {@link OssRealtimeVulnerability}. + * Creates a {@link Vulnerability} instance based on the provided + * {@link OssRealtimeVulnerability}. * * @param vulnerabilityObj the OSS vulnerability object * @return a standardized {@link Vulnerability} object @@ -157,7 +168,8 @@ private Vulnerability createVulnerability(OssRealtimeVulnerability vulnerability } /** - * Creates a {@link Location} object based on the provided {@link RealtimeLocation}. + * 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 @@ -177,7 +189,8 @@ private int getLine(RealtimeLocation location) { } /** - * Generates a unique ID for the given scan issue using line, package identifier, and version. + * Generates a unique ID for the given scan issue using line, package + * identifier, and version. * * @param scanIssue the scan issue * @return unique string identifier @@ -190,7 +203,6 @@ private String getUniqueId(ScanIssue scanIssue) { return DevAssistUtils.generateUniqueId( line, scanIssue.getPackageManager() + scanIssue.getTitle(), - scanIssue.getPackageVersion() - ); + scanIssue.getPackageVersion()); } } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java index e6d52b30..9bf83431 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/state/ScannerStateManager.java @@ -8,12 +8,13 @@ /** * Manages scanner state persistence using Eclipse preferences. - * Loads and saves which scanners are enabled/disabled and scan frequency preference. + * Loads and saves which scanners are enabled/disabled and scan frequency + * preference. */ public class ScannerStateManager { // Aligned to match the canonical plugin qualifier used across the plugin - private static final String PLUGIN_ID = "com.checkmarx.eclipse"; + private static final String PLUGIN_ID = "com.checkmarx.eclipse"; private static final String KEY_PREFIX = "pref_"; private static final String KEY_ENABLED_SUFFIX = "_enabled"; private static final String KEY_FREQUENCY = "scan.frequency"; diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java index f058c798..80834dfc 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -34,7 +34,6 @@ import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.ui.plugin.AbstractUIPlugin; -import org.eclipse.ui.texteditor.ITextEditor; import org.apache.commons.lang3.StringUtils; import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsContentProvider; @@ -46,6 +45,7 @@ import com.checkmarx.eclipse.devassist.backend.listener.CheckmarxDocumentListener; import com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob; import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.ui.findings.model.FileNodeLabel; import com.checkmarx.eclipse.devassist.ui.findings.model.ScanDetailWithPath; import com.checkmarx.eclipse.devassist.model.Location; import com.checkmarx.eclipse.devassist.model.ScanEngine; @@ -65,10 +65,11 @@ 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. + * 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 { @@ -83,6 +84,8 @@ public class CxFindingsView extends ViewPart implements IgnoredProblemsListener public static final Image FINDINGS_PROMOTIONAL_CUBE = createScaledImage("/icons/cx-one-assist-cube.png", 240); private static final Image CHECKMARX_OPEN_SETTINGS_LOGO = AbstractUIPlugin .imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, "/icons/checkmarx-80.png").createImage(); + private static final Image STAR_ICON = AbstractUIPlugin + .imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, "/icons/severity/star-action.svg").createImage(); public CxFindingsView() { super(); @@ -113,7 +116,8 @@ public void createPartControl(Composite parent) { /** * Loads an image and scales it down to the given max width (maintaining aspect - * ratio) if it is larger than that width. + * ratio) + * if it is larger than that width. */ private static Image createScaledImage(String path, int maxWidth) { Image original = AbstractUIPlugin.imageDescriptorFromPlugin(Constants.MAIN_PLUGIN_ID, path).createImage(); @@ -155,8 +159,9 @@ private void loadCachedIssues() { 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")); + // Use getInstance() to get the correct ProblemHolderService instance + // (matches the same key used by getInstance() in ProblemHolderService) + ProblemHolderService problemHolder = ProblemHolderService.getInstance(project); if (problemHolder != null) { Map> existingIssues = problemHolder.getAllScanIssues(); @@ -174,8 +179,36 @@ private void loadCachedIssues() { /** * Renders the missing credentials panel centered inside the view parent. + * Also clears all findings from the ProblemHolderService and editor annotations on logout. */ private void drawMissingCredentialsPanel(Composite parent) { + // Clear all findings from memory BEFORE disposing UI (to ensure tab title updates) + try { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects.length > 0 && projects[0].isOpen()) { + IProject project = projects[0]; + ProblemHolderService problemHolder = ProblemHolderService.getInstance(project); + if (problemHolder != null) { + problemHolder.clearAll(); + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error clearing findings on logout: " + e.getMessage()); + } + + // Reset current issues cache + currentIssues.clear(); + + // Update tab title immediately to remove problem count + setPartName(DevAssistConstants.DEVASSIST_TAB); + + // Clear all annotations from open editors + try { + com.checkmarx.eclipse.devassist.problems.ProblemDecorator.clearAllAnnotations(); + } catch (Exception e) { + System.err.println("[FINDINGS] Error clearing annotations on logout: " + e.getMessage()); + } + // Dispose all existing UI components in the view container for (Control child : parent.getChildren()) { child.dispose(); @@ -198,8 +231,8 @@ private void drawMissingCredentialsPanel(Composite parent) { btn.setText(Constants.BTN_OPEN_SETTINGS); btn.addListener(SWT.Selection, event -> { - PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn(shell, - "com.checkmarx.eclipse.properties.preferencespage", null, null); + PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn( + shell, "com.checkmarx.eclipse.properties.preferencespage", null, null); if (pref != null) { pref.open(); } @@ -230,7 +263,6 @@ private void drawFindingsPanel(Composite parent) { sashForm.setWeights(new int[] { 70, 30 }); - setupToolbar(); setupTreeListeners(); if (!currentIssues.isEmpty()) { @@ -238,11 +270,15 @@ private void drawFindingsPanel(Composite parent) { } parent.layout(true, true); + + // Setup toolbar after layout has settled to ensure proper rendering in tab bar + Display.getDefault().asyncExec(this::setupToolbar); } /** * Renders the promotional cube image and description text in the right-hand - * pane of the findings split view. + * pane + * of the findings split view. */ private void drawPromotionalPanel(Composite promotionalComposite) { GridLayout layout = new GridLayout(1, false); @@ -271,7 +307,8 @@ private void drawPromotionalPanel(Composite promotionalComposite) { if (promotionalComposite.isDisposed()) { return; } - int availableWidth = promotionalComposite.getClientArea().width - (layout.marginLeft + layout.marginRight); + int availableWidth = promotionalComposite.getClientArea().width + - (layout.marginLeft + layout.marginRight); if (availableWidth > 0 && descriptionData.widthHint != availableWidth) { descriptionData.widthHint = availableWidth; promotionalComposite.layout(true); @@ -296,8 +333,8 @@ private void subscribeToEventBroker() { .getService(org.eclipse.e4.core.services.events.IEventBroker.class); if (eventBroker == null) { - eventBroker = PlatformUI.getWorkbench() - .getService(org.eclipse.e4.core.services.events.IEventBroker.class); + eventBroker = PlatformUI.getWorkbench().getService( + org.eclipse.e4.core.services.events.IEventBroker.class); } if (eventBroker != null) { @@ -339,7 +376,8 @@ public void dispose() { 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); + .getService( + org.eclipse.e4.core.services.events.IEventBroker.class); if (eventBroker != null) { eventBroker.unsubscribe(eventHandler); @@ -433,8 +471,8 @@ public void run() { Action openPreferencesPageAction = new Action() { @Override public void run() { - PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn(shell, - "com.checkmarx.eclipse.properties.preferencespage", null, null); + PreferenceDialog pref = PreferencesUtil.createPreferenceDialogOn( + shell, "com.checkmarx.eclipse.properties.preferencespage", null, null); if (pref != null) { pref.open(); } @@ -455,6 +493,10 @@ public void run() { toolbar.update(true); getViewSite().getActionBars().updateActionBars(); + // Force layout update on the parent to ensure toolbar renders properly + if (parentComposite != null && !parentComposite.isDisposed()) { + parentComposite.layout(true, true); + } } private void setupTreeListeners() { @@ -504,7 +546,6 @@ private void navigateToIssue(ScanDetailWithPath detailWithPath) { if (filePath == null) { filePath = detailWithPath.getFilePath(); } - if (detail.getLocations() != null && !detail.getLocations().isEmpty()) { Location location = detail.getLocations().get(0); @@ -548,16 +589,17 @@ private void showIssueDetails(ScanIssue issue) { */ 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); + 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. + * 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) { @@ -588,9 +630,9 @@ private void ignoreThisFinding(ScanIssue issue) { } /** - * 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 + * 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) { @@ -654,67 +696,174 @@ private String escapeJson(String text) { private void openFileInEditor(String filePath, int lineNumber, ScanIssue issue) { try { - IFile file = ResourcesPlugin.getWorkspace().getRoot() - .getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); - if (file == null || !file.exists()) return; + + IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( + new org.eclipse.core.runtime.Path(filePath)); + + if (file == null || !file.exists()) { + + return; + } + // 1. Open file in active workbench page IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); IEditorPart editor = IDE.openEditor(page, file); + // **CRITICAL FIX: Navigation-based opens don't trigger IPartListener2 events** // Directly set up real-time scanning and apply cached decorations // Pass the editor to avoid re-searching for it (which fails on MavenPomEditor) - applyCachedDecorationsForFile(file, editor); + setupRealtimeScanningForFile(file, editor); + // 2. Ensure marker exists and explicitly set LINE_NUMBER createMarkerForIssue(file, issue); + // 3. Navigate using standard ITextEditor adapter (or fall back to marker // navigation) boolean scrolledSuccessfully = scrollToLine(editor, lineNumber); if (!scrolledSuccessfully) { highlightViaMarker(editor, file, issue); } + } catch (Exception e) { + e.printStackTrace(); } } - - + /** - * Applies cached decorations (gutter icons, underlines) when editor is opened via navigation. - * Uses the provided editor directly to avoid lookup issues with specialized editors. + * 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 applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, org.eclipse.ui.IEditorPart editor) { - if (file == null || editor == null) { - return; - } - try { - String filePath = file.getLocation().toOSString(); - org.eclipse.core.resources.IProject project = file.getProject(); - if (project == null) { - return; - } - // Get cached findings for this file - ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( - new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); - if (problemHolder == null) { - return; - } - java.util.List cachedIssues = problemHolder.getScanIssuesByFile(filePath); - if (cachedIssues == null || cachedIssues.isEmpty()) { - return; - } - // Apply decorations directly using the provided editor - applyDecorationsDirectly(editor, file, cachedIssues); - } catch (Exception e) { - System.err.println("[FINDINGS] Error applying cached decorations: " + e.getMessage()); - e.printStackTrace(); - } + private void setupRealtimeScanningForFile(org.eclipse.core.resources.IFile file, IEditorPart editor) { + if (file == null || editor == null) { + + return; + } + + try { + // Extract document for real-time scanning + org.eclipse.jface.text.IDocument document = null; + String fileName = file.getName(); + + // Try method 1: Direct ITextEditor instance check + if (editor instanceof org.eclipse.ui.texteditor.ITextEditor) { + + org.eclipse.ui.texteditor.ITextEditor textEditor = (org.eclipse.ui.texteditor.ITextEditor) editor; + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + + // Try method 2: ITextEditor Adapter pattern (for MavenPomEditor, etc.) + if (document == null) { + + org.eclipse.ui.texteditor.ITextEditor textEditor = editor + .getAdapter(org.eclipse.ui.texteditor.ITextEditor.class); + if (textEditor != null) { + + document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + } + + // Try method 3: Direct IDocument adapter (some editors provide this directly) + if (document == null) { + + document = editor.getAdapter(org.eclipse.jface.text.IDocument.class); + if (document != null) { + + } + } + + if (document == null) { + + return; + } + + // Create a scan job for this file + RealTimeScanJob scanJob = new RealTimeScanJob(file, fileName); + + // Create a document listener that reschedules the job on every keystroke + com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler scheduler = null; + if (file != null) { + try { + org.eclipse.core.resources.IProject project = file.getProject(); + if (project != null) { + scheduler = (com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler) project + .getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", + "scan-scheduler")); + } + } catch (Exception e) { + // Ignore if scheduler not available + } + } + CheckmarxDocumentListener docListener = new CheckmarxDocumentListener(fileName, scanJob, file, scheduler); + + // Register the document listener + document.addDocumentListener(docListener); + + // Apply cached decorations if findings exist for this file + // Pass the editor directly to avoid search issues with MavenPomEditor + applyCachedDecorationsForFile(file, document, editor); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] ✗ EXCEPTION during setup: " + e.getMessage()); + System.err.println("[REALTIME-SETUP] Exception type: " + e.getClass().getName()); + System.err.println("[REALTIME-SETUP] Stack trace:"); + e.printStackTrace(); + } + } + + /** + * Apply cached decorations (gutter icons, underlines) when editor is opened via + * navigation. + * Uses the provided editor directly instead of searching for it. + */ + private void applyCachedDecorationsForFile(org.eclipse.core.resources.IFile file, + org.eclipse.jface.text.IDocument document, + org.eclipse.ui.IEditorPart editor) { + if (file == null || document == null || editor == null) { + return; + } + + try { + String filePath = file.getLocation().toOSString(); + org.eclipse.core.resources.IProject project = file.getProject(); + + if (project == null) { + return; + } + + // Get cached findings for this file + ProblemHolderService problemHolder = (ProblemHolderService) project.getSessionProperty( + new org.eclipse.core.runtime.QualifiedName("com.checkmarx.eclipse.plugin", "problem-holder")); + + if (problemHolder == null) { + return; + } + + java.util.List cachedIssues = problemHolder.getScanIssuesByFile(filePath); + + if (cachedIssues == null || cachedIssues.isEmpty()) { + + return; + } + + // Apply decorations directly using the provided editor + + applyDecorationsDirectly(editor, file, cachedIssues); + + } catch (Exception e) { + System.err.println("[REALTIME-SETUP] Error applying cached decorations: " + e.getMessage()); + e.printStackTrace(); + } } /** * Apply decorations directly to the provided editor without searching for it. * This avoids issues with MavenPomEditor not being found by IFile comparison. */ - private void applyDecorationsDirectly(org.eclipse.ui.IEditorPart editor, org.eclipse.core.resources.IFile file, + 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; @@ -794,16 +943,18 @@ private com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation cr ScanIssue issue) { try { String annotationType = mapSeverityToAnnotationType(issue.getSeverity()); - return new com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation(annotationType, - issue.getTitle(), issue.getDescription()); + 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. + * Map severity to annotation type. + * Handles all 8 severity levels including OK, UNKNOWN, and IGNORED. */ private String mapSeverityToAnnotationType(String severity) { if (severity == null) { @@ -907,50 +1058,27 @@ private boolean scrollToLine(IEditorPart editor, int lineNumber) { } /** - * Ensures IMarker.LINE_NUMBER is explicitly set as a 1-based Integer attribute. + * Ensures a marker exists for this issue. Markers are now created eagerly for + * every + * detected finding as soon as it's decorated (see + * ProblemDecorator.decorateEditor(), which + * calls MarkerIssueMapper.ensureMarker() for each issue) - this remains as a + * safety net for + * the navigate-to-finding flow in case the file wasn't open (and therefore + * wasn't decorated) + * when the finding was first reported. */ private void createMarkerForIssue(IFile file, ScanIssue issue) { - 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, - (ITextEditor) null); - - } catch (org.eclipse.core.runtime.CoreException e) { - System.err.println("[FINDINGS] Error creating marker: " + e.getMessage()); - e.printStackTrace(); - } + com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.ensureMarker(file, issue); } /** * Apply highlighting to the problematic line. */ /** - * Navigate to the marker that corresponds to this issue. JDT's editor will - * automatically underline the marker and respect the marker annotation - * infrastructure (no custom hover registration needed). + * 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 { @@ -959,7 +1087,8 @@ private void highlightViaMarker(org.eclipse.ui.IEditorPart editor, IFile file, S } // Find the marker corresponding to this issue - IMarker marker = findMarkerForIssue(file, issue); + IMarker marker = com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.findMarker(file, + issue); if (marker != null && marker.exists()) { org.eclipse.ui.ide.IDE.gotoMarker(editor, marker); @@ -971,113 +1100,6 @@ private void highlightViaMarker(org.eclipse.ui.IEditorPart editor, IFile file, S } } - /** - * Find the IMarker that corresponds to a ScanIssue. Matches by file, line - * number, and optionally title. - */ - private IMarker findMarkerForIssue(IFile file, ScanIssue issue) { - if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { - return null; - } - - int issueLine = issue.getLocations().get(0).getLine(); - String issueTitle = issue.getTitle(); - - try { - IMarker[] markers = file.findMarkers("com.checkmarx.eclipse.plugin.checkmarxProblemMarker", true, - org.eclipse.core.resources.IResource.DEPTH_ZERO); - for (IMarker marker : markers) { - int markerLine = marker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); - if (markerLine == issueLine) { - // Optional: also match by message prefix for better accuracy - String markerMsg = marker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); - if (issueTitle == null || issueTitle.isEmpty() || markerMsg.contains(issueTitle)) { - return marker; - } - } - } - } catch (Exception e) { - - } - - return null; - } - - /** - * Create a marker for a ScanIssue. - * - * **CRITICAL FIX**: Markers were never being created, only searched for. This - * method creates markers on-demand when user navigates to an issue. - * - * **Works for ALL file types**: Java, Python, C++, JavaScript, YAML, XML, etc. - * Uses Eclipse's universal IMarker API (not language-specific). - * - * Marker attributes are populated using MarkerIssueMapper to store all - * ScanIssue data in marker attributes for later retrieval. - * - * @param file File to create marker in - * @param issue ScanIssue to create marker for - */ -// private void createMarkerForIssue(IFile file, ScanIssue issue) { -// if (file == null || issue == null || issue.getLocations() == null || issue.getLocations().isEmpty()) { -// -// return; -// } -// -// try { -// -// -// -// -// -// -// -// -// // Step 1: Check if marker already exists for this issue -// IMarker existingMarker = findMarkerForIssue(file, issue); -// if (existingMarker != null && existingMarker.exists()) { -// -// return; -// } -// -// // Step 2: Create new marker using Eclipse's universal IMarker API -// // **KEY**: Uses IMarker.PROBLEM which works for ALL file types -// // - NOT language-specific (works for Java, Python, C++, JS, YAML, etc.) -// // - Marker appears in Eclipse's Problems View -// // - Can be navigated with IDE.gotoMarker() -// IMarker newMarker = file.createMarker("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); -// -// -// // Step 3: Populate marker attributes using MarkerIssueMapper -// // This stores all ScanIssue data in marker for later retrieval -// com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper.populateMarker(newMarker, issue); -// -// -// // Step 4: Verify marker creation -// if (newMarker.exists()) { -// String markerMsg = newMarker.getAttribute(org.eclipse.core.resources.IMarker.MESSAGE, ""); -// int markerLine = newMarker.getAttribute(org.eclipse.core.resources.IMarker.LINE_NUMBER, -1); -// int markerSeverity = newMarker.getAttribute(org.eclipse.core.resources.IMarker.SEVERITY, -1); -// -// -// -// -// -// -// -// } else { -// -// } -// -// } catch (org.eclipse.core.runtime.CoreException e) { -// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ CoreException creating marker: " + e.getMessage()); -// e.printStackTrace(); -// } catch (Exception e) { -// System.err.println("[FINDINGS] [MARKER-CREATE] ✗ Error creating marker: " + e.getMessage()); -// e.printStackTrace(); -// } -// } - private void showContextMenu(MouseEvent e) { ISelection selection = treeViewer.getSelection(); if (!(selection instanceof IStructuredSelection)) { @@ -1098,29 +1120,32 @@ private void showContextMenu(MouseEvent e) { 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() { + // Menu Item 1: Fix with AI Assist + org.eclipse.swt.widgets.MenuItem fixWithAIItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + fixWithAIItem.setImage(STAR_ICON); + fixWithAIItem.setText(DevAssistConstants.FIX_WITH_CXONE_ASSIST); + fixWithAIItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - new RemediationManager().viewDetails(issue, DevAssistConstants.QUICK_FIX); + new RemediationManager().fixWithCxOneAssist(issue, DevAssistConstants.QUICK_FIX); } }); - // Menu Item 2: Fix with AI Assist - org.eclipse.swt.widgets.MenuItem fixWithAIItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); - fixWithAIItem.setText("Fix with AI Assist"); - fixWithAIItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { + // Menu Item 2: View Details + org.eclipse.swt.widgets.MenuItem viewDetailsItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); + viewDetailsItem.setImage(STAR_ICON); + viewDetailsItem.setText(DevAssistConstants.VIEW_DETAILS_FIX_NAME); + viewDetailsItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { - new RemediationManager().fixWithCxOneAssist(issue, DevAssistConstants.QUICK_FIX); + new RemediationManager().viewDetails(issue, DevAssistConstants.QUICK_FIX); } }); // Menu Item 3: Ignore This Finding org.eclipse.swt.widgets.MenuItem ignoreItem = new org.eclipse.swt.widgets.MenuItem(menu, SWT.PUSH); - ignoreItem.setText("Ignore This Finding"); + ignoreItem.setImage(STAR_ICON); + ignoreItem.setText(DevAssistConstants.IGNORE_THIS_VULNERABILITY_FIX_NAME); ignoreItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { @@ -1132,7 +1157,8 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { // 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.setImage(STAR_ICON); + ignoreAllItem.setText(DevAssistConstants.IGNORE_ALL_OF_THIS_TYPE_FIX_NAME); ignoreAllItem.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { @@ -1177,7 +1203,6 @@ private void refreshTreeWithFilter() { VulnerabilityFilterState filterState = VulnerabilityFilterState.getInstance(); Map> filteredIssues = new HashMap<>(); - int totalBefore = 0; int totalAfter = 0; for (String filePath : currentIssues.keySet()) { @@ -1185,7 +1210,6 @@ private void refreshTreeWithFilter() { if (issues == null) continue; - totalBefore += issues.size(); List filtered = new java.util.ArrayList<>(); for (ScanIssue issue : issues) { @@ -1245,8 +1269,8 @@ private void refreshTreeWithFilter() { // 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 (element instanceof FileNodeLabel) { + FileNodeLabel fileNode = (FileNodeLabel) element; if (filteredIssues.containsKey(fileNode.getFilePath())) { validExpanded.add(element); } @@ -1263,7 +1287,55 @@ private void refreshTreeWithFilter() { } // Update view title with problem count - setPartName("Checkmarx One Assist Findings " + totalAfter); + if (totalAfter > 0) { + setPartName(DevAssistConstants.DEVASSIST_TAB+ " " + totalAfter); + } else { + setPartName(DevAssistConstants.DEVASSIST_TAB); + } + + // Apply decorations to open editors for all filtered findings + // This ensures annotations are in the annotation model for hover to find them + applyDecorationsToOpenEditors(filteredIssues); + } + + /** + * Apply decorations to all open editors that have findings in filteredIssues. + * This ensures annotations are present in the annotation model when the Findings View + * displays cached results, so hover can find them without waiting for a new scan. + */ + private void applyDecorationsToOpenEditors(Map> filteredIssues) { + if (filteredIssues == null || filteredIssues.isEmpty()) { + return; + } + + try { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page == null) { + return; + } + + for (String filePath : filteredIssues.keySet()) { + List issues = filteredIssues.get(filePath); + if (issues == null || issues.isEmpty()) { + continue; + } + + // Find if this file is currently open in an editor + try { + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFileForLocation(new org.eclipse.core.runtime.Path(filePath)); + if (file != null && file.exists()) { + // Trigger decoration for this file's open editor (if any) + com.checkmarx.eclipse.devassist.problems.ProblemDecorator.decorateEditor(file, issues); + } + } catch (Exception e) { + // Log but continue with other files + System.err.println("[FINDINGS] Error decorating file " + filePath + ": " + e.getMessage()); + } + } + } catch (Exception e) { + System.err.println("[FINDINGS] Error applying decorations to open editors: " + e.getMessage()); + } } /** @@ -1274,9 +1346,6 @@ private void refreshTreeWithFilter() { public void refreshTree(Map> issues) { if (issues == null) return; - - int totalIssues = issues.values().stream().filter(java.util.Objects::nonNull).mapToInt(List::size).sum(); - // Log issues by severity Map severityCounts = new HashMap<>(); issues.values().forEach(issueList -> { @@ -1291,14 +1360,12 @@ public void refreshTree(Map> issues) { }); this.currentIssues = issues; - // ✅ Thread-safe dispatching for background updates org.eclipse.swt.widgets.Display.getDefault().asyncExec(() -> { if (treeViewer != null && treeViewer.getControl() != null && !treeViewer.getControl().isDisposed()) { refreshTreeWithFilter(); } }); - } @Override @@ -1324,42 +1391,6 @@ public void onIgnoredProblemsChanged() { } } - 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); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java index 028e9561..bbe11f49 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/actions/VulnerabilityFilterAction.java @@ -2,6 +2,8 @@ import org.eclipse.jface.action.Action; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + /** * Base toggle action for severity filters in the Findings view. */ @@ -14,20 +16,32 @@ public interface IFilterChangeListener { void onFilterChanged(); } - public VulnerabilityFilterAction(String severity, IFilterChangeListener listener) { - super(severity, Action.AS_CHECK_BOX); - this.severity = severity; - this.filterChangeListener = listener; + 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 label with theme-aware styling indicator + String label = severity.substring(0, 1).toUpperCase() + severity.substring(1); + setText(label); - // Set initial state - setChecked(VulnerabilityFilterState.getInstance().hasFilter(severity)); - } + // Load icon with theme-aware variant + String iconSuffix = DevAssistUtils.isDarkTheme() ? "_20_dark.svg" : "_20.svg"; + setImageDescriptor(org.eclipse.ui.plugin.AbstractUIPlugin + .imageDescriptorFromPlugin("com.checkmarx.eclipse.devassist", "icons/" + severity + iconSuffix)); + + // Apply theme-aware tooltip + String tooltip = "Filter " + severity + " severity findings"; + if (DevAssistUtils.isDarkTheme()) { + tooltip += " (Light selection highlight in dark theme)"; + } else { + tooltip += " (Gray selection highlight in light theme)"; + } + setToolTipText(tooltip); + + // Set initial state + setChecked(VulnerabilityFilterState.getInstance().hasFilter(severity)); + } @Override public void run() { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java index aebaeb3c..e0cf85e8 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsAnnotation.java @@ -3,19 +3,28 @@ import java.util.ArrayList; import java.util.List; import org.eclipse.jface.text.source.Annotation; +import com.checkmarx.eclipse.devassist.model.ScanIssue; public class FindingsAnnotation extends Annotation { private String title; private String description; + private ScanIssue scanIssue; private List buttons = new ArrayList<>(); public FindingsAnnotation(String type, String title, String description) { - super(type, false, title); + super(type, false, null); this.title = title; this.description = description; } + public FindingsAnnotation(String type, String title, String description, ScanIssue scanIssue) { + super(type, false, null); + this.title = title; + this.description = description; + this.scanIssue = scanIssue; + } + public void addButton(String label, Runnable action) { buttons.add(new AnnotationButton(label, action)); } @@ -24,8 +33,17 @@ public List getButtons() { return buttons; } - public String getTitle() { return title; } - public String getDescription() { return description; } + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public ScanIssue getScanIssue() { + return scanIssue; + } public static class AnnotationButton { public String label; diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java index b1bed62e..a4f54826 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/editor/FindingsEditorOverlay.java @@ -11,7 +11,8 @@ /** * Manages highlighting and underlining of problematic code lines in the editor. - * Provides visual feedback for findings by underlining vulnerable code with severity-based colors. + * Provides visual feedback for findings by underlining vulnerable code with + * severity-based colors. * * Supports: * - Red wavy underline for CRITICAL/HIGH issues @@ -32,7 +33,7 @@ public class FindingsEditorOverlay { * Highlight a problematic line in the editor. * * @param editor The TextEditor to highlight in - * @param issue The scan issue containing location information + * @param issue The scan issue containing location information */ public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { try { @@ -45,13 +46,13 @@ public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ISourceViewer.class); if (viewer == null) { - + return; } IDocument document = viewer.getDocument(); if (document == null || lineNumber < 0 || lineNumber >= document.getNumberOfLines()) { - + return; } @@ -62,21 +63,18 @@ public static void highlightIssueLine(TextEditor editor, ScanIssue issue) { // Create annotation for the line String annotationType = getAnnotationTypeForSeverity(issue.getSeverity()); - FindingsAnnotation annotation = new FindingsAnnotation(annotationType, issue.getTitle(), issue.getDescription()); + FindingsAnnotation annotation = new FindingsAnnotation(annotationType, issue.getTitle(), + issue.getDescription(), issue); Position position = new Position(lineStartOffset, lineEndOffset - lineStartOffset); // Add annotation to model IAnnotationModel annotationModel = viewer.getAnnotationModel(); if (annotationModel != null) { annotationModel.addAnnotation(annotation, position); - - - - - + } } catch (BadLocationException e) { - + } } @@ -106,9 +104,8 @@ public static void clearHighlights(TextEditor editor) { } }); - } catch (Exception e) { - + } } @@ -137,4 +134,3 @@ private static String getAnnotationTypeForSeverity(String severity) { } } } - diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxAnnotationHover.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxAnnotationHover.java new file mode 100644 index 00000000..2ce1edbc --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxAnnotationHover.java @@ -0,0 +1,740 @@ +package com.checkmarx.eclipse.devassist.ui.findings.hover; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.internal.text.html.BrowserInformationControl; +import org.eclipse.jface.text.AbstractReusableInformationControlCreator; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DefaultInformationControl; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IInformationControl; +import org.eclipse.jface.text.IInformationControlCreator; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextHoverExtension; +import org.eclipse.jface.text.ITextHoverExtension2; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.browser.ProgressEvent; +import org.eclipse.swt.browser.ProgressListener; + +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.remediation.RemediationLinkHandler; +import com.checkmarx.eclipse.devassist.ui.findings.editor.FindingsAnnotation; +import com.checkmarx.eclipse.devassist.ui.findings.marker.MarkerIssueMapper; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.HtmlEscapeUtil; +import org.eclipse.jface.resource.JFaceColors; + +/** + * Line hover for Checkmarx findings, contributed to the JDT Java editor via + * org.eclipse.jdt.ui.javaEditorTextHovers (the only public Eclipse extension + * point for adding a hover to an editor this plugin does not own). Renders the + * Checkmarx problem description as HTML, and appends the text of any OTHER + * annotations already present on the same line (JDT compiler errors, other + * linters, etc.) so hovering never hides existing information for the line - it + * only adds to it. This mirrors how JetBrains merges multiple inspection + * results (HighlightInfo entries) into a single hover popup. + *

+ * Must implement IJavaEditorTextHover (not just ITextHover) because JDT's hover + * framework (JavaEditorTextHoverDescriptor.createTextHover()) casts contributed + * hover classes to IJavaEditorTextHover. + *

+ * NOTE: JDT gates which contributed hovers are actually active via user + * preferences (Preferences > Java > Editor > Hovers, keyed by this hover's id + * and a modifier-key/state-mask). Registering the extension makes this hover + * available and selectable, but does NOT enable it by default - on a fresh + * install the user must check "Checkmarx Finding" in that preference page (and + * give it the "None"/combination slot to see it on a plain mouse hover with no + * modifier key). Until then, hovering shows Eclipse's default combination + * annotation hover instead (plain, unstyled marker text). + */ +public class CheckmarxAnnotationHover implements IJavaEditorTextHover, ITextHoverExtension2, ITextHoverExtension { + + /** + * Hover popup sizing is now automatic - content determines the size. + * The popup will automatically expand to fit the content and allows + * user resizing via dragging, consistent with non-Java editor hovers. + */ + + private static final CheckmarxProblemDescriptionFormatter PROBLEM_DESCRIPTRO = new CheckmarxProblemDescriptionFormatter(); + + /** + * Creates the small (~6-line) preview control shown on the initial mouse hover. + * Mirrors JDT's own AbstractAnnotationHover/JavadocHover: the browser control + * returned here overrides getInformationPresenterControlCreator() to point at + * the enlarged PresenterControlCreator below - without that override, + * AbstractInformationControlManager.canReplace() always returns false, so + * moving the mouse toward the popup can never "enrich" it into the bigger, + * reachable control and the popup instead closes on the next pixel of mouse + * movement outside the hovered line. + *

+ * Must extend AbstractReusableInformationControlCreator (not a bare + * IInformationControlCreator lambda/anonymous class) so the SAME browser widget + * is reused across repeated hover computations - otherwise + * AbstractInformationControlManager.getInformationControl() disposes and + * recreates the control on every mouse-hover tick (it only skips that when the + * creator implements IInformationControlCreatorExtension, which the reusable + * base class does), which was cutting the browser off mid-render before it + * could finish laying out the HTML. + */ + private static final class HoverControlCreator extends AbstractReusableInformationControlCreator { + private final IInformationControlCreator presenterControlCreator; + + HoverControlCreator(IInformationControlCreator presenterControlCreator) { + this.presenterControlCreator = presenterControlCreator; + } + + @Override + public IInformationControl doCreateInformationControl(Shell parent) { + String tooltipAffordance = EditorsUI.getTooltipAffordanceString(); + if (BrowserInformationControl.isAvailable(parent)) { + BrowserInformationControl control = new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, + tooltipAffordance) { + @Override + public IInformationControlCreator getInformationPresenterControlCreator() { + return presenterControlCreator; + } + + @Override + public void setSizeConstraints(int maxWidth, int maxHeight) { + // Use default sizing - content determines popup size, user can resize by dragging + super.setSizeConstraints(maxWidth, maxHeight); + } + }; + control.setBackgroundColor( + JFaceColors.getInformationViewerBackgroundColor(parent.getDisplay())); + + control.setForegroundColor( + JFaceColors.getInformationViewerForegroundColor(parent.getDisplay())); + setupActionHandler(control); + return control; + } + return new DefaultInformationControl(parent, tooltipAffordance) { + @Override + public IInformationControlCreator getInformationPresenterControlCreator() { + return presenterControlCreator; + } + }; + } + + private void setupActionHandler(BrowserInformationControl control) { + try { + java.lang.reflect.Field browserField = BrowserInformationControl.class.getDeclaredField("fBrowser"); + browserField.setAccessible(true); + org.eclipse.swt.browser.Browser browser = (org.eclipse.swt.browser.Browser) browserField.get(control); + if (browser != null && !browser.isDisposed()) { + CxLogger.info("[HOVER] HoverControlCreator: Setting up LocationListener for action buttons"); + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changing: " + event.location); + if (event.location.contains("#cxonedevassist/")) { + CxLogger.info("[HOVER] Blocking remediation action URL: " + event.location); + event.doit = false; + } + } + + @Override + public void changed(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changed: " + event.location); + int actionIndex = event.location.indexOf("#cxonedevassist/"); + if (actionIndex >= 0) { + event.doit = false; + String linkData = event.location.substring(actionIndex + 16); // +16 for "#cxonedevassist/" + CxLogger.info("[HOVER] Extracted link data: " + linkData); + handleHoverAction(linkData); + } + } + }); + browser.addProgressListener(new ProgressListener() { + @Override + public void changed(ProgressEvent event) { + // no-op: only the final completed() matters here + } + + @Override + public void completed(ProgressEvent event) { + // Content is now laid out - popup size is determined by content + // No fixed size constraints applied + } + }); + CxLogger.info("[HOVER] LocationListener added successfully to HoverControlCreator"); + } else { + CxLogger.info("[HOVER] HoverControlCreator: Browser is null or disposed"); + } + } catch (Exception e) { + CxLogger.error("Failed to setup action handler for hover buttons (HoverControlCreator)", e); + } + } + } + + private static void handleHoverAction(String action) { + CxLogger.info("[HOVER] Action button clicked: " + action); + + if (currentFinding == null) { + CxLogger.info("[HOVER] No finding context available for action: " + action); + return; + } + + RemediationLinkHandler linkHandler = new RemediationLinkHandler(); + boolean handled = linkHandler.handleLink(action, currentFinding); + + if (!handled) { + CxLogger.info("[HOVER] Unknown or unhandled action: " + action); + } + } + + /** + * Creates the enlarged, resizable, focusable control that replaces the small + * preview once the mouse moves toward it - this is what actually lets the user + * read the full finding and reach the action links. + */ + private static final class PresenterControlCreator extends AbstractReusableInformationControlCreator { + @Override + public IInformationControl doCreateInformationControl(Shell parent) { + if (BrowserInformationControl.isAvailable(parent)) { + BrowserInformationControl control = new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, + true) { + + @Override + public void setSizeConstraints(int maxWidth, int maxHeight) { + // Use default sizing - content determines popup size, user can resize by dragging + super.setSizeConstraints(maxWidth, maxHeight); + } + }; + control.setBackgroundColor( + JFaceColors.getInformationViewerBackgroundColor(parent.getDisplay())); + + control.setForegroundColor( + JFaceColors.getInformationViewerForegroundColor(parent.getDisplay())); + setupActionHandler(control); + return control; + } + return new DefaultInformationControl(parent, true); + } + + private void setupActionHandler(BrowserInformationControl control) { + try { + java.lang.reflect.Field browserField = BrowserInformationControl.class.getDeclaredField("fBrowser"); + browserField.setAccessible(true); + org.eclipse.swt.browser.Browser browser = (org.eclipse.swt.browser.Browser) browserField.get(control); + if (browser != null && !browser.isDisposed()) { + CxLogger.info("[HOVER] PresenterControlCreator: Setting up LocationListener for action buttons"); + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changing: " + event.location); + if (event.location.contains("#cxonedevassist/")) { + CxLogger.info("[HOVER] Blocking remediation action URL: " + event.location); + event.doit = false; + } + } + + @Override + public void changed(LocationEvent event) { + CxLogger.info("[HOVER] LocationListener.changed: " + event.location); + int actionIndex = event.location.indexOf("#cxonedevassist/"); + if (actionIndex >= 0) { + event.doit = false; + String linkData = event.location.substring(actionIndex + 16); // +16 for "#cxonedevassist/" + CxLogger.info("[HOVER] Extracted link data: " + linkData); + handleHoverAction(linkData); + } + } + }); + browser.addProgressListener(new ProgressListener() { + @Override + public void changed(ProgressEvent event) { + // no-op: only the final completed() matters here + } + + @Override + public void completed(ProgressEvent event) { + // Content is now laid out - popup size is determined by content + // No fixed size constraints applied + } + }); + CxLogger.info("[HOVER] LocationListener added successfully to PresenterControlCreator"); + } else { + CxLogger.info("[HOVER] PresenterControlCreator: Browser is null or disposed"); + } + } catch (Exception e) { + CxLogger.error("Failed to setup action handler for hover buttons (PresenterControlCreator)", e); + } + } + } + + private IInformationControlCreator hoverControlCreator; + private IInformationControlCreator presenterControlCreator; + private static ScanIssue currentFinding; + + @Override + public void setEditor(IEditorPart editor) { + // No editor-specific state needed: getHoverInfo2() derives everything + // it needs from the ITextViewer/ISourceViewer passed at hover time. + } + + /** + * Returns the whole line as the hover's "subject area" rather than a zero-width + * point at the cursor. JFace keeps the popup alive only while the mouse stays + * inside this region, so a zero-width region gave the mouse nowhere to go - it + * dismissed on the next pixel of movement, before the browser control could + * finish laying out the full HTML and before the mouse could travel toward the + * popup to interact with it. + */ + @Override + public IRegion getHoverRegion(ITextViewer textViewer, int offset) { + IDocument document = textViewer.getDocument(); + if (document != null) { + try { + return document.getLineInformationOfOffset(offset); + } catch (BadLocationException e) { + // fall through to point region below + } + } + return new Region(offset, 0); + } + + /** + * Without this, JFace falls back to a plain-text control and the HTML markup + * produced by getHoverInfo2()/CheckmarxProblemDescriptionFormatter would either + * show as literal tags or be flattened to plain text - the same + * BrowserInformationControl mechanism JDT's own Javadoc/Problem hovers use to + * render rich HTML. + *

+ * Returns a cached instance (not a fresh one per call) because + * TextViewerHoverManager.computeInformation() calls this on every hover + * computation and re-registers whatever it gets via + * setCustomInformationControlCreator() - a stable, + * AbstractReusableInformationControlCreator-based instance lets that call + * recognize "same creator" and keep reusing the existing control instead of + * tearing it down and rebuilding it each time. + */ + @Override + public IInformationControlCreator getHoverControlCreator() { + if (hoverControlCreator == null) { + hoverControlCreator = new HoverControlCreator(getPresenterControlCreator()); + } + return hoverControlCreator; + } + + private IInformationControlCreator getPresenterControlCreator() { + if (presenterControlCreator == null) { + presenterControlCreator = new PresenterControlCreator(); + } + return presenterControlCreator; + } + + @Override + public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { + Object info = getHoverInfo2(textViewer, hoverRegion); + return info != null ? info.toString() : null; + } + + @Override + public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) { + long startTime = System.currentTimeMillis(); + try { + if (!(textViewer instanceof ISourceViewer)) { + return null; + } + ISourceViewer sourceViewer = (ISourceViewer) textViewer; + IAnnotationModel model = sourceViewer.getAnnotationModel(); + IDocument document = sourceViewer.getDocument(); + if (model == null || document == null) { + return null; + } + + int lineNumber; + try { + lineNumber = document.getLineOfOffset(hoverRegion.getOffset()); + } catch (Exception e) { + CxLogger.error("CheckmarxAnnotationHover: failed to get line number", e); + return null; + } + + StringBuilder html = new StringBuilder(); +// html.append(""); + + String backgroundColor = getHoverBackgroundColorHex(); + String foregroundColor = getHoverForegroundColorHex(); + + html.append("") + .append("") + .append("") + .append("") + .append(""); + + // Determine text color for dynamic elements in the formatter + // (e.g., ASCA/IAC vulnerability titles). This is done on the UI thread, + // so it's safe to use from the formatter via parameter passing. + String textColorForElements = getTextColorForTheme(); + + Set seenMarkerIds = new HashSet<>(); + // Tracks scanIssueIds already rendered via a FindingsAnnotation (the live, + // fully-populated ScanIssue) so a MarkerAnnotation for the SAME issue - which + // Eclipse creates the moment a finding is clicked in the Findings view, and + // which coexists indefinitely alongside the FindingsAnnotation in the same + // annotation model - doesn't render the finding a second time with only its + // root title/description (MarkerIssueMapper's marker-attribute reconstruction + // is inherently lossier than the live object). + Set renderedIssueIds = new HashSet<>(); + // Fallback dedup key for issues without scanIssueId (e.g., OSS): title+line. + // Mirrors MarkerIssueMapper.findMarker()'s line+title heuristic for issues + // without a stable scanIssueId. + Set renderedIssueKeys = new HashSet<>(); + List checkmarxSections = new ArrayList<>(); + List otherMessages = new ArrayList<>(); + + List lineAnnotations = new ArrayList<>(); + Iterator it = model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation annotation = it.next(); + if (annotation == null || annotation.isMarkedDeleted()) { + continue; + } + + Position position = null; + try { + position = model.getPosition(annotation); + } catch (Exception e) { + continue; + } + + if (position == null || !isOnLine(document, position, lineNumber)) { + continue; + } + + lineAnnotations.add(annotation); + } + + // Pass 1: FindingsAnnotation first - it carries the live ScanIssue (full + // vulnerabilities list intact), so it takes priority over any MarkerAnnotation + // reconstruction of the same underlying issue. + for (Annotation annotation : lineAnnotations) { + if (!(annotation instanceof FindingsAnnotation)) { + continue; + } + FindingsAnnotation findingsAnn = (FindingsAnnotation) annotation; + ScanIssue scanIssue = findingsAnn.getScanIssue(); + if (scanIssue == null) { + continue; + } + currentFinding = scanIssue; + CxLogger.info("[HOVER] Captured ScanIssue for action handlers: " + scanIssue.getTitle()); + + if (scanIssue.getScanIssueId() != null && !scanIssue.getScanIssueId().isEmpty()) { + renderedIssueIds.add(scanIssue.getScanIssueId()); + CxLogger.warning("[HOVER] Pass 1 - Added to renderedIssueIds: " + scanIssue.getScanIssueId()); + } else { + // Fallback dedup key for issues without scanIssueId. + // Use enhanced key that includes engine-specific identifiers (e.g., package@version for OSS) + String enhancedKey = getEnhancedFallbackKey(scanIssue); + String fallbackKey = buildFallbackDedupKey(enhancedKey, lineNumber); + renderedIssueKeys.add(fallbackKey); + CxLogger.warning("[HOVER] Pass 1 - Added to renderedIssueKeys: " + fallbackKey); + } + + // Use consolidated formatter for both ASCA/IAC (iterates vulnerabilities) + // and other engines (uses root ScanIssue attributes) + try { + String sectionHtml = PROBLEM_DESCRIPTRO.formatDescriptionHtml(scanIssue, true, textColorForElements); + if (!sectionHtml.isEmpty()) { + checkmarxSections.add("

" + sectionHtml + "
"); + ScanEngine engine = scanIssue.getScanEngine(); + String engineName = (engine != null) ? engine.toString() : "UNKNOWN"; + CxLogger.info("[HOVER] " + engineName + ": Rendered ScanIssue via formatter - " + + scanIssue.getTitle()); + } + } catch (Exception e) { + CxLogger.error("[HOVER] Error formatting FindingsAnnotation: " + e.getMessage(), e); + } + } + + // Pass 2: MarkerAnnotation (Checkmarx markers) and everything else (JDT/other + // linters). A Checkmarx marker is skipped here if its issue was already + // rendered in pass 1 above. +// for (Annotation annotation : lineAnnotations) { +// if (annotation instanceof FindingsAnnotation +// && ((FindingsAnnotation) annotation).getScanIssue() != null) { +// continue; // already handled in pass 1 +// } +// +// if (annotation instanceof MarkerAnnotation) { +// MarkerAnnotation markerAnnotation = (MarkerAnnotation) annotation; +// IMarker marker = markerAnnotation.getMarker(); +// if (isCheckmarxMarker(marker)) { +// Long id = marker.getId(); +// if (seenMarkerIds.contains(id)) { +// continue; +// } +// seenMarkerIds.add(id); +// +// String issueId = MarkerIssueMapper.getIssueId(marker); +// // Check primary dedup key (issueId) first +// if (!issueId.isEmpty() && renderedIssueIds.contains(issueId)) { +// CxLogger.info("[HOVER] Skipping marker " + id + ": issue " + issueId +// + " already rendered via FindingsAnnotation"); +// continue; +// } +// +// // Check enhanced fallback dedup key for issues without stable issueId. +// // Extract title and version from marker message for robust matching. +// String markerMessage = marker.getAttribute(IMarker.MESSAGE, ""); +// String markerTitle = extractTitleFromMarkerMessage(markerMessage); +// String markerVersion = extractVersionFromMarkerMessage(markerMessage); +// +// // Build enhanced key matching Pass 1 logic +// String enhancedMarkerKey = markerTitle; +// if (!markerVersion.isEmpty()) { +// // Include version for engines like OSS that report it +// enhancedMarkerKey = markerTitle + "@" + markerVersion; +// } +// +// String fallbackKey = buildFallbackDedupKey(enhancedMarkerKey, lineNumber); +// if (renderedIssueKeys.contains(fallbackKey)) { +// CxLogger.info("[HOVER] Skipping marker " + id + ": issue (title=" + markerTitle +// + ", version=" + markerVersion + ", line=" + (lineNumber + 1) +// + ") already rendered via FindingsAnnotation"); +// continue; +// } +// +// CxLogger.warning("[HOVER] Pass 2 - Checking marker: " + fallbackKey +// + " | Available keys: " + renderedIssueKeys); +// +// String section = buildCheckmarxSection(marker, id, textColorForElements); +// if (!section.isEmpty()) { +// checkmarxSections.add(section); +// if (!issueId.isEmpty()) { +// renderedIssueIds.add(issueId); +// CxLogger.warning("[HOVER] Pass 2 - Rendered marker with ID: " + issueId); +// } else { +// // Track via enhanced fallback key if issueId is empty +// renderedIssueKeys.add(fallbackKey); +// CxLogger.warning("[HOVER] Pass 2 - Rendered marker with fallback key: " + fallbackKey); +// } +// } +// } else { +// // Non-Checkmarx marker (JDT, other linters) - collect as other message +// String message = annotation.getText(); +// if (message != null && !message.isEmpty()) { +// otherMessages.add(message); +// } +// } +// continue; +// } +// +// // Collect other linter/annotation messages (JDT, etc.) that aren't handled +// // above +// String message = annotation.getText(); +// if (message != null && !message.isEmpty()) { +// otherMessages.add(message); +// } +// } + + CxLogger.info("[HOVER] Line " + (lineNumber + 1) + ": Found " + checkmarxSections.size() + + " Checkmarx section(s), " + otherMessages.size() + " other message(s)"); + + if (checkmarxSections.isEmpty()) { + CxLogger.info("[HOVER] No Checkmarx findings to display, returning null"); + return null; + } + + for (int i = 0; i < checkmarxSections.size(); i++) { + if (i > 0) { + html.append("
"); + } + html.append(checkmarxSections.get(i)); + } + + if (!otherMessages.isEmpty()) { + html.append("
"); + for (String message : otherMessages) { + html.append("
").append(HtmlEscapeUtil.escape(message)) + .append("
"); + } + } + + html.append(""); + return html.toString(); + } finally { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > 100) { + CxLogger.info("CheckmarxAnnotationHover.getHoverInfo2() took " + elapsed + "ms"); + } + } + } + + + private String buildCheckmarxSection(IMarker marker, Long markerId, String textColor) { + try { + ScanIssue issue = MarkerIssueMapper.fromMarker(marker); + if (issue == null) { + CxLogger.info("[HOVER] Marker " + markerId + ": Failed to extract ScanIssue from marker"); + return ""; + } + currentFinding = issue; + CxLogger.info("[HOVER] Captured ScanIssue for action handlers from marker: " + issue.getTitle()); + // Use consolidated formatter with clickable actions enabled (same as + // FindingsAnnotation path) + String html = "
" + PROBLEM_DESCRIPTRO.formatDescriptionHtml(issue, true, textColor) + "
"; + CxLogger.info("[HOVER] Marker " + markerId + ": Built HTML section for issue: " + issue.getTitle()); + return html; + } catch (Exception e) { + CxLogger.error("CheckmarxAnnotationHover: failed to build hover content for marker " + markerId, e); + return ""; + } + } + + private boolean isCheckmarxMarker(IMarker marker) { + try { + return marker != null && marker.exists() + && marker.isSubtypeOf("com.checkmarx.eclipse.plugin.checkmarxProblemMarker"); + } catch (Exception e) { + return false; + } + } + + private boolean isOnLine(IDocument document, Position position, int lineNumber) { + try { + int startLine = document.getLineOfOffset(position.getOffset()); + int endLine = document.getLineOfOffset(position.getOffset() + Math.max(position.getLength() - 1, 0)); + return lineNumber >= startLine && lineNumber <= endLine; + } catch (Exception e) { + return false; + } + } + + private static String getHoverBackgroundColorHex() { + Display display = Display.getDefault(); + final String[] colorHex = new String[1]; + + Runnable runnable = () -> { + if (DevAssistUtils.isDarkTheme()) { + colorHex[0] = "#000000"; + } else { + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + + colorHex[0] = String.format("#%02x%02x%02x", + bg.getRed(), + bg.getGreen(), + bg.getBlue()); + } + }; + + if (Display.getCurrent() == display) { + runnable.run(); + } else { + display.syncExec(runnable); + } + + return colorHex[0]; + } + private static String getHoverForegroundColorHex() { + Display display = Display.getDefault(); + + final String[] colorHex = new String[1]; + + Runnable runnable = () -> { + Color fg = JFaceColors.getInformationViewerForegroundColor(display); + + colorHex[0] = String.format("#%02x%02x%02x", + fg.getRed(), + fg.getGreen(), + fg.getBlue()); + }; + + if (Display.getCurrent() == display) { + runnable.run(); + } else { + display.syncExec(runnable); + } + + return colorHex[0]; + } + + private static String buildFallbackDedupKey(String title, int lineNumber) { + return (title != null ? title : "") + "|" + lineNumber; + } + + private static String getTextColorForTheme() { + Display display = Display.getDefault(); + final String[] textColor = new String[1]; + + Runnable runnable = () -> { + if (DevAssistUtils.isDarkTheme()) { + textColor[0] = "#FFFFFF"; + } else { + textColor[0] = "#000000"; + } + }; + + if (Display.getCurrent() == display) { + runnable.run(); + } else { + display.syncExec(runnable); + } + + return textColor[0]; + } + + /** + * Builds an enhanced fallback key that uniquely identifies a scan issue + * when no stable scanIssueId is available. Incorporates engine-specific + * identifiers for better deduplication. + * + * @param scanIssue the scan issue to generate a key for + * @return a unique identifier string for the issue + */ + private static String getEnhancedFallbackKey(ScanIssue scanIssue) { + if (scanIssue == null || scanIssue.getTitle() == null) { + return ""; + } + + ScanEngine engine = scanIssue.getScanEngine(); + + // For OSS packages, include version for uniqueness + if (engine == ScanEngine.OSS) { + String version = scanIssue.getPackageVersion(); + if (version != null && !version.isEmpty()) { + return scanIssue.getTitle() + "@" + version; + } + } + + // Default: use title only + return scanIssue.getTitle(); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java new file mode 100644 index 00000000..455bbff8 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/hover/CheckmarxProblemDescriptionFormatter.java @@ -0,0 +1,516 @@ +package com.checkmarx.eclipse.devassist.ui.findings.hover; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.FileLocator; +import java.net.URL; +import java.util.Arrays; +import com.checkmarx.eclipse.devassist.backend.SeverityLevel; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.HtmlEscapeUtil; +import static com.checkmarx.eclipse.devassist.ui.findings.hover.CheckmarxProblemDescriptionFormatter.InlineStyle.*; +import static com.checkmarx.eclipse.devassist.utils.DevAssistConstants.SEPERATOR; + +/** + * Formats a ScanIssue as an HTML fragment for display in the editor's line + * hover. Consolidates all HTML rendering for both marker-based and live + * FindingsAnnotation paths. + *

+ * Supports two action link modes: - Clickable (enableClickableActions=true): + * renders for LocationListener interception - + * Informational (enableClickableActions=false): renders plain text with Ctrl+1 + * hint + *

+ * For ASCA/IAC issues that group multiple vulnerabilities on the same line, + * renders one block per vulnerability instead of collapsing to root attributes. + */ +public final class CheckmarxProblemDescriptionFormatter { + + private static final Map DESCRIPTION_ICON = new LinkedHashMap<>(); + + private static final String COUNT = "COUNT"; + private static final String PACKAGE = "Package"; + private static final String DEV_ASSIST = "DevAssist"; + private static final String CONTAINER = "Container"; + + public CheckmarxProblemDescriptionFormatter() { + initIconsMap(); + } + + private static void initIconsMap() { + DESCRIPTION_ICON.put(SeverityLevel.MALICIOUS.getSeverity(), + getImage(DevAssistConstants.ImagePaths.MALICIOUS_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.CRITICAL.getSeverity(), + getImage(DevAssistConstants.ImagePaths.CRITICAL_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.HIGH.getSeverity(), getImage(DevAssistConstants.ImagePaths.HIGH_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.MEDIUM.getSeverity(), getImage(DevAssistConstants.ImagePaths.MEDIUM_PNG)); + DESCRIPTION_ICON.put(SeverityLevel.LOW.getSeverity(), getImage(DevAssistConstants.ImagePaths.LOW_PNG)); + + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.CRITICAL.getSeverity()), + getImage(DevAssistConstants.ImagePaths.CRITICAL_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.HIGH.getSeverity()), + getImage(DevAssistConstants.ImagePaths.HIGH_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.MEDIUM.getSeverity()), + getImage(DevAssistConstants.ImagePaths.MEDIUM_16_PNG)); + DESCRIPTION_ICON.put(getSeverityCountIconKey(SeverityLevel.LOW.getSeverity()), + getImage(DevAssistConstants.ImagePaths.LOW_16_PNG)); + + DESCRIPTION_ICON.put(PACKAGE, getImage(DevAssistConstants.ImagePaths.PACKAGE_PNG)); + DESCRIPTION_ICON.put(DEV_ASSIST, getImage(DevAssistConstants.ImagePaths.DEV_ASSIST_PNG)); + DESCRIPTION_ICON.put(CONTAINER, getImage(DevAssistConstants.ImagePaths.CONTAINER_PNG)); + } + + /** + * Build the HTML body (without outer html/body tags) describing the issue, + * suitable for embedding inside a BrowserInformationControl or merging with + * other annotations' hover text on the same line. + *

+ * Supports both clickable action links (for CheckmarxAnnotationHover's + * BrowserInformationControl) and informational-only links (for marker + * resolution fallback). + * + * @param issue the scan issue + * @param enableClickableActions if true, renders action links as #action:... + * for LocationListener interception; if false, + * renders as informational text with Ctrl+1 hint + * @param textColor text color in hex format (e.g., "#000000" for dark themes, + * "#FFFFFF" for light), or null to use inherited color + * @return HTML fragment + */ + public String formatDescriptionHtml(ScanIssue scanIssue, boolean enableClickableActions, String textColor) { + StringBuilder descBuilder = new StringBuilder(); + + // DevAssist image + descBuilder.append(TABLE_WITH_TR).append("") + .append(DESCRIPTION_ICON.get(DEV_ASSIST)).append(""); + descBuilder.append("


"); + + // For ASCA and IAC multiple violations + appendMultipleViolationsTitle(descBuilder, scanIssue, textColor); + + switch (scanIssue.getScanEngine()) { + case OSS: + buildOSSDescription(descBuilder, scanIssue, textColor); + break; + case ASCA: + buildASCADescription(descBuilder, scanIssue, textColor); + break; + case SECRETS: + buildSecretsDescription(descBuilder, scanIssue, textColor); + break; + case IAC: + buildIACDescription(descBuilder, scanIssue, textColor); + break; + case CONTAINERS: + buildContainerDescription(descBuilder, scanIssue); + break; + default: + buildDefaultDescription(descBuilder, scanIssue); + } + if (scanIssue.getScanEngine() != ScanEngine.IAC && scanIssue.getScanEngine() != ScanEngine.ASCA) { + buildRemediationActionsSection(descBuilder, scanIssue.getScanIssueId(), scanIssue.getScanEngine().name()); + } + return descBuilder.toString(); + } + + /** + * Builds the OSS description for the provided scan issue and appends it to the + * given StringBuilder. This method incorporates severity-specific formatting, + * including handling for malicious packages, and assembles the description with + * the package header and vulnerability details. + * + * @param descBuilder the StringBuilder to which the formatted OSS description + * will be appended + * @param scanIssue the ScanIssue object containing information about the + * scanned issue, including its severity, vulnerabilities, + * and related details + */ + private void buildOSSDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + buildPackageMessage(descBuilder, scanIssue, textColor); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + /** + * Builds the package header section of a description for a scan issue and + * appends it to the provided StringBuilder. This method formats information + * about the scan issue's severity, title, and package version, and includes an + * associated image icon representing the issue. + * + * @param descBuilder the StringBuilder to which the formatted package header + * information will be appended + * @param scanIssue the ScanIssue object containing details about the issue + * such as severity, title, and package version + */ + private static void buildPackageMessage(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String secondaryText = DevAssistConstants.SEVERITY_PACKAGE; + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + String iconKey = PACKAGE; + if (scanIssue.getSeverity().equalsIgnoreCase(SeverityLevel.MALICIOUS.getSeverity())) { + secondaryText = PACKAGE; + iconKey = scanIssue.getSeverity(); + } + String icon = getSeverityIconHtml(iconKey, ICON_INLINE_STYLE); + + descBuilder.append(TABLE_WITH_TR).append("").append(icon) + .append("").append("").append("

").append("") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append("@") + .append(HtmlEscapeUtil.escape(scanIssue.getPackageVersion())).append("").append(" - ").append(HtmlEscapeUtil.escape(scanIssue.getSeverity())) + .append(" ").append(HtmlEscapeUtil.escape(secondaryText)).append("

"); + } + + /** + * Builds the vulnerability section of a scan issue description and appends it + * to the provided StringBuilder. This method processes the list of + * vulnerabilities associated with the scan issue, categorizes them by severity, + * and includes detailed descriptions for specific vulnerabilities where + * applicable. + * + * @param descBuilder the StringBuilder to which the formatted vulnerability + * section will be appended + * @param scanIssue the ScanIssue object containing details about the scan, + * including associated vulnerabilities + */ + private void buildVulnerabilitySection(StringBuilder descBuilder, ScanIssue scanIssue) { + List vulnerabilityList = scanIssue.getVulnerabilities(); + if (vulnerabilityList == null || vulnerabilityList.isEmpty()) { + return; + } + descBuilder.append("
").append(TABLE_WITH_TR); + Map vulnerabilityCount = getVulnerabilityCount(vulnerabilityList); + DESCRIPTION_ICON.forEach((severity, iconPath) -> { + Long count = vulnerabilityCount.get(severity); + if (count != null && count > 0) { + descBuilder.append("") + .append(DESCRIPTION_ICON.get(getSeverityCountIconKey(severity))).append("") + .append("") + .append(count).append(""); + } + }); + descBuilder.append("
"); + } + + /** + * ASCA description. Format: [Title for multiple issues] [Severity Icon] Title + * (bold) - description - SAST vulnerability + */ + private void buildASCADescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) { + String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE); + descBuilder.append(TABLE_WITH_TR_IAC_ASCA) + .append("").append(severityIcon) + .append(""); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder.append("") + .append("
") + .append("

").append("") + .append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("").append(" - ") + .append(HtmlEscapeUtil.escape(vulnerability.getDescription())).append(" - SAST vulnerability").append("

") + .append("
"); + buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(), scanIssue.getScanEngine().name()); + } + } + + + /** + * Secrets description. Format: [Severity Icon] Title (bold) - Secret finding + */ + private void buildSecretsDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String icon = getSeverityIconHtml(scanIssue.getSeverity(), ICON_INLINE_STYLE); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder.append(TABLE_WITH_TR).append("").append(icon) + .append("").append("").append("

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

"); + } + + /** + * IAC description (image header + vulnerability description with Title). + */ + private void buildIACDescription(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + for (Vulnerability vulnerability : scanIssue.getVulnerabilities()) { + String severityIcon = getSeverityIconHtml(vulnerability.getSeverity(), ICON_INLINE_STYLE); + descBuilder.append(TABLE_WITH_TR_IAC_ASCA) + .append("").append(severityIcon) + .append(""); + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + descBuilder + .append("") + .append("
") + .append("").append(HtmlEscapeUtil.escape(vulnerability.getTitle())).append("").append(" - ") + .append(HtmlEscapeUtil.escape(vulnerability.getActualValue())).append(" ") + .append(HtmlEscapeUtil.escape(vulnerability.getDescription())) + .append(" IaC vulnerability") + .append("
"); + buildRemediationActionsSection(descBuilder, vulnerability.getVulnerabilityId(), + scanIssue.getScanEngine().name()); + } + } + + /** + * Container description (image header + vulnerability counts). + */ + private void buildContainerDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + buildImageHeader(descBuilder, scanIssue); + buildVulnerabilitySection(descBuilder, scanIssue); + } + + /** + * Builds the default description for a scan issue and appends it to the + * provided StringBuilder. This method formats basic details about the scan + * issue, including its title and description. + * + * @param descBuilder the StringBuilder to which the formatted default + * description will be appended + * @param scanIssue the ScanIssue object containing details about the issue + * such as title and description + */ + /** + * Default fallback description. + */ + private void buildDefaultDescription(StringBuilder descBuilder, ScanIssue scanIssue) { + descBuilder.append("
").append(scanIssue.getTitle()).append(" -").append(scanIssue.getDescription()); + } + + /** + * Container image header. + */ + private void buildImageHeader(StringBuilder descBuilder, ScanIssue scanIssue) { + String icon = getSeverityIconHtml(CONTAINER, ICON_INLINE_STYLE); + + descBuilder.append(TABLE_WITH_TR).append("").append(icon) + .append("").append("").append("

").append("") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append("@") + .append(HtmlEscapeUtil.escape(scanIssue.getImageTag())).append("").append("

"); + } + + /** + * Calculates the count of vulnerabilities grouped by their severity levels. + * This method processes a list of vulnerabilities, retrieves their severity, + * and returns a map where the keys are severity levels and the values are the + * counts. + * + * @param vulnerabilityList the list of vulnerabilities to be grouped and + * counted by severity + * @return a map where the key is the severity level and the value is the count + * of vulnerabilities at that severity + */ + private Map getVulnerabilityCount(List vulnerabilityList) { + return vulnerabilityList.stream().map(Vulnerability::getSeverity) + .collect(Collectors.groupingBy(severity -> severity, Collectors.counting())); + } + + /** + * Legacy overload for backward compatibility: defaults to informational action + * links (non-clickable) and no text color override. + */ + public String formatDescriptionHtml(ScanIssue issue) { + return formatDescriptionHtml(issue, false, null); + } + + + /** + * Builds the remediation actions section of the description. + * + * @param descBuilder {@link StringBuilder} object to add the remediation + * actions section to. + * @param scanIssueId {@link String} object containing the remediation actions + * section data. + */ + private void buildRemediationActionsSection(StringBuilder descBuilder, String scanIssueId, String engineName) { + String buttonStyle = "color: #4470EC; cursor: pointer; " + TITLE_FONT_SIZE + TITLE_FONT_FAMILY + + CELL_LINE_HEIGHT_STYLE + "white-space: nowrap; margin:0; padding:0;"; + + // Add CSS for hover effect with underline - more specific selector with !important to ensure it applies + descBuilder.append(""); + + descBuilder.append( + "") + .append("") + .append("").append(""); + if (engineName.equalsIgnoreCase(String.valueOf(ScanEngine.OSS)) + || engineName.equalsIgnoreCase(String.valueOf(ScanEngine.CONTAINERS))) { + descBuilder.append("").append("
").append("") + .append(DevAssistUtils.getAssistQuickFixName()).append("").append("").append(DevAssistConstants.VIEW_DETAILS_FIX_NAME) + .append("") + .append("") + .append(DevAssistConstants.IGNORE_THIS_VULNERABILITY_FIX_NAME).append("") + .append("") + + .append(DevAssistConstants.IGNORE_ALL_OF_THIS_TYPE_FIX_NAME); + } + descBuilder.append("

"); + } + + + /** + * Injects inline styles into an existing HTML image tag. + */ + private static String getSeverityIconHtml(String key, String extraStyle) { + String imgTag = DESCRIPTION_ICON.getOrDefault(key, ""); + + if (imgTag == null || imgTag.isEmpty()) { + return ""; + } + + if (imgTag.contains("style='")) { + return imgTag.replaceFirst("style='", "style='" + extraStyle); + } else if (imgTag.contains("style=\"")) { + return imgTag.replaceFirst("style=\"", "style=\"" + extraStyle); + } else { + int insertPos = imgTag.indexOf("/>"); + + return insertPos > 0 + ? imgTag.substring(0, insertPos) + " style='" + extraStyle + "'" + imgTag.substring(insertPos) + : imgTag; + } + } + + /** + * Inline styles matching JetBrains' ProblemDescription.InlineStyle. Ensures + * visual consistency with JetBrains plugin design. + */ + static class InlineStyle { + + private InlineStyle() { + } + + // Table layout: icon (20px) in first column, content in second column + static final String TABLE_WITH_TR = ""; + static final String TABLE_WITH_TR_IAC_ASCA = "
"; + + static final String TABLE_WITH_TR_FULL_WIDTH = "
"; + + // Typography styles + static final String TITLE_FONT_FAMILY = "font-family: sans-serif"; + static final String TITLE_FONT_SIZE = "font-size:12px;"; + static final String CELL_LINE_HEIGHT_STYLE = "line-height:16px;vertical-align:middle;"; + + // Secondary text (severity labels like "SAST vulnerability", "IaC + // vulnerability") + static final String SECONDARY_SPAN_STYLE = "display:inline-block;vertical-align:middle;line-height:16px;font-size:11px;color:#ADADAD;" + + "font-family:system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;"; + + // Icon column style (20px wide, right-padded) + static final String ICON_COLUMN_STYLE = "width:20px;padding:0 6px 0 0;vertical-align:middle;"; + + // Content column style + static final String CONTENT_COLUMN_STYLE = "padding:0 4px;white-space:normal;" + TITLE_FONT_SIZE + + TITLE_FONT_FAMILY + CELL_LINE_HEIGHT_STYLE; + + /** + * Default inline severity icon style used consistently across all engines. + */ + static final String ICON_INLINE_STYLE = "display:inline-block;vertical-align:middle;max-height:16px;line-height:16px;"; + } + + /** + * Appends multiple violations title for ASCA and IAC engines when there are + * multiple vulnerabilities. This method adds a formatted title showing the + * number of violations detected. + * + * @param descBuilder the StringBuilder to append the title to + * @param scanIssue the ScanIssue containing information about vulnerabilities + */ + private static void appendMultipleViolationsTitle(StringBuilder descBuilder, ScanIssue scanIssue, String textColor) { + String colorStyle = textColor != null && !textColor.isEmpty() ? "color:" + textColor + ";" : ""; + if (scanIssue.getVulnerabilities() == null || scanIssue.getVulnerabilities().size() <= 1) { + return; + } + boolean isASCAOrIAC = scanIssue.getScanEngine() == ScanEngine.ASCA + || scanIssue.getScanEngine() == ScanEngine.IAC; + if (isASCAOrIAC) { + descBuilder.append(TABLE_WITH_TR).append("
").append("

") + .append(HtmlEscapeUtil.escape(scanIssue.getTitle())).append(" Checkmarx One Assist") + .append("

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