diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java new file mode 100644 index 00000000..be9e4309 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java @@ -0,0 +1,92 @@ +package io.snyk.eclipse.plugin.html; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import org.eclipse.jface.resource.ColorRegistry; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.themes.ITheme; +import org.eclipse.ui.themes.IThemeManager; + +public class BaseHtmlProvider { + private final Random random = new Random(); + private final Map colorCache = new HashMap<>(); + private String nonce = ""; + + public String getCss() { + return ""; + } + + public String getJs() { + return ""; + } + + public String getInitScript() { + return ""; + } + + public String getNonce() { + if(!nonce.isEmpty()) { + return nonce; + } + String allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder nonceBuilder = new StringBuilder(32); + for (int i = 0; i < 32; i++) { + nonceBuilder.append(allowedChars.charAt(random.nextInt(allowedChars.length()))); + } + nonce = nonceBuilder.toString(); + return nonce; + } + + public String replaceCssVariables(String html) { + // Build the CSS with the nonce + String nonce = getNonce(); + String css = ""; + html = html.replace("${ideStyle}", css); + html = html.replace("", css); + html = html.replace("var(--default-font)", " ui-sans-serif, \"SF Pro Text\", \"Segoe UI\", \"Ubuntu\", Tahoma, Geneva, Verdana, sans-serif;"); + + + // Replace CSS variables with actual color values + html = html.replace("var(--text-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_TEXT_COLOR", "#000000")); + html = html.replace("var(--background-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_BG_START", "#FFFFFF")); + html = html.replace("var(--border-color)", getColorAsHex( "org.eclipse.ui.workbench.ACTIVE_TAB_BORDER_COLOR", "#CCCCCC")); + html = html.replace("var(--link-color)", getColorAsHex("org.eclipse.ui.workbench.HYPERLINK_COLOR", "#0000FF")); + html = html.replace("var(--horizontal-border-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_HIGHLIGHT_BORDER_COLOR", "#CCCCCC")); + html = html.replace("var(--code-background-color)", getColorAsHex("org.eclipse.ui.workbench.CODE_BACKGROUND_COLOR", "#F0F0F0")); + + html = html.replace("${headerEnd}", ""); + html = html.replace("${nonce}", nonce); + html = html.replace("ideNonce", nonce); + html = html.replace("${ideScript}", ""); + + return html; + } + + public String getColorAsHex(String colorKey, String defaultColor) { + return colorCache.computeIfAbsent(colorKey, key -> { + ColorRegistry colorRegistry = getColorRegistry(); + Color color = colorRegistry.get(colorKey); + if (color == null) { + return defaultColor; + } else { + RGB rgb = color.getRGB(); + return String.format("#%02x%02x%02x", rgb.red, rgb.green, rgb.blue); + } + }); + } + + private ColorRegistry colorRegistry; + private ColorRegistry getColorRegistry() { + if(colorRegistry != null) { + return colorRegistry; + } + IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager(); + ITheme currentTheme = themeManager.getCurrentTheme(); + colorRegistry = currentTheme.getColorRegistry(); + return colorRegistry; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java new file mode 100644 index 00000000..ebd4e23b --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java @@ -0,0 +1,90 @@ +package io.snyk.eclipse.plugin.html; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.themes.ITheme; +import org.eclipse.ui.themes.IThemeManager; + +public class CodeHtmlProvider extends BaseHtmlProvider { + private static CodeHtmlProvider instance = new CodeHtmlProvider(); + + public static CodeHtmlProvider getInstance() { + if (instance == null) { + synchronized (CodeHtmlProvider.class) { + if (instance == null) { + instance = new CodeHtmlProvider(); + } + } + } + return instance; + } + + @Override + public String getInitScript() { + String themeScript = getThemeScript(); + String initScript = super.getInitScript(); + return initScript + "\n" + """ + function navigateToIssue(e, target) { + e.preventDefault(); + var filePath = target.getAttribute('file-path'); + var startLine = target.getAttribute('start-line'); + var endLine = target.getAttribute('end-line'); + var startCharacter = target.getAttribute('start-character'); + var endCharacter = target.getAttribute('end-character'); + window.openInEditor(filePath, startLine, endLine, startCharacter, endCharacter); + } + var navigatableLines = document.getElementsByClassName('data-flow-clickable-row'); + for(var i = 0; i < navigatableLines.length; i++) { + navigatableLines[i].onclick = function(e) { + navigateToIssue(e, this); + return false; + }; + } + if(document.getElementById('position-line')) { + document.getElementById('position-line').onclick = function(e) { + var target = navigatableLines[0]; + if(target) { + navigateToIssue(e, target); + } + } + } + // Disable AIfix + if(document.getElementById('ai-fix-wrapper') && document.getElementById('no-ai-fix-wrapper')){ + document.getElementById('ai-fix-wrapper').className = 'hidden'; + document.getElementById('no-ai-fix-wrapper').className = ''; + } + """ + themeScript; + } + + private ITheme currentTheme; + private ITheme getCurrentTheme() { + if(currentTheme != null) { + return currentTheme; + } + IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager(); + currentTheme = themeManager.getCurrentTheme(); + return currentTheme; + } + private String getThemeScript() { + ITheme currentTheme = getCurrentTheme(); + String themeId = currentTheme.getId().toLowerCase(); + + boolean isDarkTheme = themeId.contains("dark"); + boolean isHighContrast = themeId.contains("highcontrast") || themeId.contains("high-contrast"); + + String themeScript = "var isDarkTheme = " + isDarkTheme + ";\n" + + "var isHighContrast = " + isHighContrast + ";\n" + + "document.body.classList.add(isHighContrast ? 'high-contrast' : (isDarkTheme ? 'dark' : 'light'));"; + return themeScript; + } + + @Override + public String replaceCssVariables(String html) { + html = super.replaceCssVariables(html); + + // Replace CSS variables with actual color values + html = html.replace("var(--example-line-removed-color)", super.getColorAsHex("org.eclipse.ui.workbench.lineRemovedColor", "#ff0000")); + html = html.replace("var(--example-line-added-color)", super.getColorAsHex("org.eclipse.ui.workbench.lineAddedColor", "#00ff00")); + + return html; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java new file mode 100644 index 00000000..3db5d826 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java @@ -0,0 +1,20 @@ +package io.snyk.eclipse.plugin.html; + +import io.snyk.eclipse.plugin.domain.ProductConstants; + +public class HtmlProviderFactory { + + public static BaseHtmlProvider GetHtmlProvider(String product) + { + switch (product) { + case ProductConstants.DISPLAYED_CODE_SECURITY: + case ProductConstants.DISPLAYED_CODE_QUALITY: + return CodeHtmlProvider.getInstance(); + case ProductConstants.DISPLAYED_OSS: + return OssHtmlProvider.getInstance(); + case ProductConstants.DISPLAYED_IAC: + return IacHtmlProvider.getInstance(); + } + return null; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java new file mode 100644 index 00000000..f54f32c0 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java @@ -0,0 +1,15 @@ +package io.snyk.eclipse.plugin.html; + +public class IacHtmlProvider extends BaseHtmlProvider { + private static IacHtmlProvider instance = new IacHtmlProvider(); + public static IacHtmlProvider getInstance() { + if (instance == null) { + synchronized (IacHtmlProvider.class) { + if (instance == null) { + instance = new IacHtmlProvider(); + } + } + } + return instance; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java new file mode 100644 index 00000000..b943853c --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java @@ -0,0 +1,22 @@ +package io.snyk.eclipse.plugin.html; + +public class OssHtmlProvider extends BaseHtmlProvider { + private static OssHtmlProvider instance = new OssHtmlProvider(); + public static OssHtmlProvider getInstance() { + if (instance == null) { + synchronized (OssHtmlProvider.class) { + if (instance == null) { + instance = new OssHtmlProvider(); + } + } + } + return instance; + } + @Override + public String replaceCssVariables(String html) { + html = super.replaceCssVariables(html); + html = html.replace("var(--container-background-color)", super.getColorAsHex("org.eclipse.ui.workbench.CODE_BACKGROUND_COLOR", "#F0F0F0")); + + return html; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java new file mode 100644 index 00000000..fbc49c7d --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java @@ -0,0 +1,133 @@ +package io.snyk.eclipse.plugin.views.snyktoolview; + +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeNode; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.swt.SWT; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.program.Program; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.Bundle; + +import io.snyk.eclipse.plugin.html.HtmlProviderFactory; +import io.snyk.eclipse.plugin.utils.ResourceUtils; + +public class BrowserHandler { + private Browser browser; + public BrowserHandler(Browser browser) { + this.browser = browser; + } + + public void initialize() { + new BrowserFunction(browser, "openInEditor") { + @SuppressWarnings("restriction") + @Override + public Object function(Object[] arguments) { + if (arguments.length != 5) { + return null; + } + String filePath = (String) arguments[0]; + var fileUri = Paths.get(filePath).toUri().toASCIIString(); + int startLine = Integer.parseInt(arguments[1].toString()); + int endLine = Integer.parseInt(arguments[2].toString()); + int startCharacter = Integer.parseInt(arguments[3].toString()); + int endCharacter = Integer.parseInt(arguments[4].toString()); + + Display.getDefault().asyncExec(() -> { + try { + Position startPosition = new Position(startLine, startCharacter); + Position endPosition = new Position(endLine, endCharacter); + Range range = new Range(startPosition, endPosition); + + var location = new Location(fileUri, range); + LSPEclipseUtils.openInEditor(location); + + } catch (Exception e) { + e.printStackTrace(); + } + }); + return null; + } + }; + + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + String url = event.location; + if(url.startsWith("http")) { + event.doit = false; + Program.launch(url); + } + } + + @Override + public void changed(LocationEvent event) { + } + }); + + initBrowserText(); + } + + public void updateBrowserContent(String text) { + String htmlContent = generateHtmlContent(text); + browser.setText(htmlContent); + } + + public CompletableFuture updateBrowserContent(TreeNode node) { + // Generate HTML content based on the selected node + if (!(node instanceof IssueTreeNode)) return CompletableFuture.completedFuture(null); + browser.setText("Loading..."); + + return CompletableFuture.supplyAsync(() -> { + return generateHtmlContent(node); + }) + .thenAccept(htmlContent -> { + Display.getDefault().asyncExec(() -> { + var product = ((ProductTreeNode) node.getParent().getParent()).getProduct(); + var htmlProvider = HtmlProviderFactory.GetHtmlProvider(product); + var content = htmlProvider.replaceCssVariables(htmlContent); + browser.setText(content); + browser.execute(htmlProvider.getInitScript()); + }); + }); + + } + + public String generateHtmlContent(TreeNode node) { + if (node instanceof BaseTreeNode) { + return ((BaseTreeNode) node).getDetails(); + } + return ""; + } + + public String generateHtmlContent(String text) { + return "" + text + "

"; + } + + public void initBrowserText() { + String snykWarningText = Platform.getResourceString(Platform.getBundle("io.snyk.eclipse.plugin"), + "%snyk.trust.dialog.warning.text"); + + Bundle bundle = Platform.getBundle("io.snyk.eclipse.plugin"); + String base64Image = ResourceUtils.getBase64Image(bundle, "logo_snyk.png"); + + browser.setText(" " + + " " + + "Snyk for Eclipse
" + "Snyk Logo" + "

Welcome to Snyk for Eclipse

" + + "

\n" + snykWarningText + "\n" + ""); + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java index 6a10928e..6b8aa95b 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java @@ -1,13 +1,6 @@ -/** - * - */ package io.snyk.eclipse.plugin.views.snyktoolview; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.action.IMenuManager; @@ -18,12 +11,19 @@ import org.eclipse.jface.viewers.TreeNode; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.program.Program; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; @@ -35,6 +35,7 @@ import org.eclipse.ui.part.ViewPart; import org.osgi.framework.Bundle; +import io.snyk.eclipse.plugin.html.HtmlProviderFactory; import io.snyk.eclipse.plugin.properties.preferences.Preferences; import io.snyk.eclipse.plugin.utils.ResourceUtils; import io.snyk.eclipse.plugin.views.snyktoolview.providers.TreeContentProvider; @@ -57,7 +58,7 @@ public class SnykToolView extends ViewPart implements ISnykToolView { private TreeViewer treeViewer; private Browser browser; private BaseTreeNode rootObject = new RootNode(); - + private BrowserHandler browserHandler; private final static Shell SHELL = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); @Override @@ -86,8 +87,8 @@ public void createPartControl(Composite parent) { // Create Browser // SWT.EDGE will be ignored if OS not windows and will be set to SWT.NONE. browser = new Browser(sashForm, SWT.EDGE); - initBrowserText(); - + browserHandler = new BrowserHandler(browser); + browserHandler.initialize(); // Set sash weights sashForm.setWeights(new int[] { 1, 2 }); @@ -96,19 +97,19 @@ public void createPartControl(Composite parent) { @SuppressWarnings("restriction") @Override public void selectionChanged(SelectionChangedEvent event) { - IStructuredSelection selection = (IStructuredSelection) event.getSelection(); - if (!selection.isEmpty()) { + Display.getDefault().asyncExec(() -> { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + if (selection.isEmpty()) return; TreeNode node = (TreeNode) selection.getFirstElement(); - updateBrowserContent(node); + browserHandler.updateBrowserContent(node); if (node instanceof IssueTreeNode) { IssueTreeNode issueTreeNode = (IssueTreeNode) node; FileTreeNode fileNode = (FileTreeNode) issueTreeNode.getParent(); LSPEclipseUtils.open(fileNode.getPath().toUri().toASCIIString(), issueTreeNode.getIssue().getLSP4JRange()); - } - } - } - }); + } + }); + }}); } private void registerTreeContextMeny(Composite parent) { @@ -118,43 +119,6 @@ private void registerTreeContextMeny(Composite parent) { parent.setMenu(menu); } - private void updateBrowserContent(TreeNode node) { - // Generate HTML content based on the selected node - String htmlContent = generateHtmlContent(node); - browser.setText(htmlContent); - } - - private void updateBrowserContent(String text) { - String htmlContent = generateHtmlContent(text); - browser.setText(htmlContent); - } - - private String generateHtmlContent(TreeNode node) { - if (node instanceof BaseTreeNode) { - return ((BaseTreeNode) node).getDetails(); - } - return ""; - } - - private String generateHtmlContent(String text) { - return "" + text + "

"; - } - - private void initBrowserText() { - String snykWarningText = Platform.getResourceString(Platform.getBundle("io.snyk.eclipse.plugin"), - "%snyk.trust.dialog.warning.text"); - - Bundle bundle = Platform.getBundle("io.snyk.eclipse.plugin"); - String base64Image = ResourceUtils.getBase64Image(bundle, "logo_snyk.png"); - - browser.setText(" " - + " " - + "Snyk for Eclipse
" + "Snyk Logo" + "

Welcome to Snyk for Eclipse

" - + "

\n" + snykWarningText + "\n" + ""); - } - @Override public void setFocus() { treeViewer.getControl().setFocus(); @@ -190,34 +154,14 @@ public void addFileNode(ProductTreeNode parent, FileTreeNode toBeAdded) { @Override public void addInfoNode(ProductTreeNode parent, InfoTreeNode toBeAdded) { - List list = new ArrayList<>(); - var children = parent.getChildren(); - if (children != null) { - list = Arrays.stream(children).map(it -> (BaseTreeNode) it).collect(Collectors.toList()); - } - toBeAdded.setParent(parent); - int insertIndex = GetLastInfoNodeIndex(list); - list.add(insertIndex, toBeAdded); - parent.setChildren(list.toArray(new BaseTreeNode[0])); + parent.addChild(toBeAdded); Display.getDefault().asyncExec(() -> { this.treeViewer.refresh(parent, true); }); } - private int GetLastInfoNodeIndex(List list) { - int insertIndex = 0; - for (int i = 0; i < list.size(); i++) { - if (list.get(i) instanceof InfoTreeNode) { - insertIndex += 1; - } else { - break; - } - } - return insertIndex; - } - @Override public ProductTreeNode getProductNode(String product, String folderPath) { if (product == null || folderPath == null) { @@ -327,4 +271,4 @@ private void addCommandIfNotPresent(IMenuManager menu, String commandId) { } } -} \ No newline at end of file +} diff --git a/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java b/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java index 1658f75a..013dec62 100644 --- a/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java +++ b/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java @@ -4,11 +4,13 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import io.snyk.eclipse.plugin.domain.ProductConstants; import io.snyk.languageserver.protocolextension.messageObjects.scanResults.Issue; +import io.snyk.languageserver.protocolextension.messageObjects.scanResults.IssueComparator; public class SnykIssueCache { private final Map> codeSecurityIssues = new ConcurrentHashMap<>(); @@ -75,8 +77,8 @@ public Collection getIssues(String path, String displayProduct) { * @param issues The collection of issues to add */ public void addCodeIssues(String path, Collection issues) { - var qualityIssues = new HashSet(issues.size()); - var securityIssues = new HashSet(issues.size()); + var qualityIssues = new TreeSet(new IssueComparator(issues)); + var securityIssues = new TreeSet(new IssueComparator(issues)); for (Issue issue : issues) { if (issue.additionalData().isSecurityType()) { securityIssues.add(issue); @@ -138,7 +140,7 @@ public void removeCodeIssuesForPath(String path) { */ public void addOssIssues(String path, Collection issues) { if (issues.size() > 0) { - ossIssues.put(path, issues); + ossIssues.put(path, new TreeSet(new IssueComparator(issues))); } else { ossIssues.remove(path); } @@ -173,7 +175,7 @@ public void removeOssIssuesForPath(String path) { */ public void addIacIssues(String path, Collection issues) { if (issues.size() > 0) { - iacIssues.put(path, issues); + iacIssues.put(path, new TreeSet(new IssueComparator(issues))); } else { iacIssues.remove(path); } @@ -208,7 +210,7 @@ public long getTotalCount(String product) { return getCacheByDisplayProduct(product).values().stream().flatMap(Collection::stream).count(); } - private Map> getCacheByDisplayProduct(String displayProduct) { + public Map> getCacheByDisplayProduct(String displayProduct) { switch (displayProduct) { case ProductConstants.DISPLAYED_OSS: return ossIssues; diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java index 79ed4fab..dcf94a27 100644 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java @@ -27,7 +27,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -99,11 +99,9 @@ import io.snyk.languageserver.protocolextension.messageObjects.SnykScanParam; import io.snyk.languageserver.protocolextension.messageObjects.SnykTrustedFoldersParams; import io.snyk.languageserver.protocolextension.messageObjects.scanResults.Issue; -import io.snyk.languageserver.protocolextension.messageObjects.scanResults.IssueSorter; @SuppressWarnings("restriction") public class SnykExtendedLanguageClient extends LanguageClientImpl { - private static final String SNYK_CODE_CONSISTENT_IGNORES = "snykCodeConsistentIgnores"; private ProgressManager progressManager = new ProgressManager(this); private final ObjectMapper om = new ObjectMapper(); private TaskProcessor taskProcessor; @@ -133,19 +131,17 @@ private void registerRefreshFeatureFlagsTask() { public void refreshFeatureFlags() { boolean enableConsistentIgnores = getFeatureFlagStatus(FeatureFlagConstants.SNYK_CODE_CONSISTENT_IGNORES); - Preferences.getInstance().store(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED, - Boolean.valueOf(enableConsistentIgnores).toString()); - - updateIgnoresButtons(); + toggleIgnores(enableConsistentIgnores); } - private void updateIgnoresButtons() { + private void toggleIgnores(Boolean enableConsistentIgnores) { + Preferences.getInstance().store(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED, + Boolean.valueOf(enableConsistentIgnores).toString()); PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { var snykToolView = SnykStartup.getView(); if (snykToolView != null) snykToolView.toggleIgnoresButtons(); }); - } private void createIssueCaches() { @@ -323,12 +319,12 @@ public void hasAuthenticated(HasAuthenticatedParam param) { if (differentToken) { prefs.store(Preferences.AUTH_TOKEN_KEY, newToken); } - + if (!Preferences.getInstance().isTest()) { configurationUpdater.configurationChanged(); refreshFeatureFlags(); } - + if (!newToken.isBlank() && PlatformUI.isWorkbenchRunning()) { enableSnykViewRunActions(); } @@ -379,7 +375,9 @@ public void snykScan(SnykScanParam param) { case SCAN_STATE_SUCCESS: scanState.setScanInProgress(inProgressKey, false); for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { + productTreeNode.reset(); addInfoNodes(productTreeNode, param.getFolderPath(), issueCache); + populateFileAndIssueNodes(productTreeNode, param.getFolderPath(), issueCache); } break; case SCAN_STATE_ERROR: @@ -502,7 +500,7 @@ private void addInfoNodes(ProductTreeNode productNode, String folderPath, SnykIs } if (totalCount > 0 && ignoredCount == totalCount - && pref.getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) + && pref.getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) && pref.getBooleanPref(FILTER_IGNORES_SHOW_OPEN_ISSUES)) { toolView.addInfoNode(productNode, new InfoTreeNode(ISnykToolView.IGNORED_ISSUES_FILTERED_BUT_AVAILABLE)); @@ -529,20 +527,19 @@ public CompletableFuture publishDiagnostics316(PublishDiagnostics316Param return; } - var productTreeNodes = populateIssueCache(param, filePath); - populateFileAndIssueNodes(filePath, productTreeNodes); + populateIssueCache(param, filePath); }); } - private void populateFileAndIssueNodes(String filePath, Set nodes) { - for (ProductTreeNode productTreeNode : nodes) { - var issueCache = IssueCacheHolder.getInstance().getCacheInstance(filePath); - var issues = issueCache.getIssues(filePath, productTreeNode.getProduct()); - issues = IssueSorter.sortIssuesBySeverity(issues); - issues = filterIgnoredIssues(issues); - if (issues.isEmpty()) + private void populateFileAndIssueNodes(ProductTreeNode productTreeNode, String folderPath, SnykIssueCache issueCache) { + var cacheHashMap = Collections.unmodifiableMap(issueCache.getCacheByDisplayProduct(productTreeNode.getProduct())); + for (var kv : cacheHashMap.entrySet()) { + var fileName = kv.getKey(); + var issues = new ArrayList<>(kv.getValue()); + issues = filterIgnoredIssues(issues); + if(issues.isEmpty()) continue; - FileTreeNode fileNode = new FileTreeNode(filePath); + FileTreeNode fileNode = new FileTreeNode(fileName); toolView.addFileNode(productTreeNode, fileNode); for (Issue issue : issues) { toolView.addIssueNode(fileNode, new IssueTreeNode(issue)); @@ -550,8 +547,7 @@ private void populateFileAndIssueNodes(String filePath, Set nod } } - private Collection filterIgnoredIssues(Collection issueList) - { + private ArrayList filterIgnoredIssues(ArrayList issueList) { final boolean includeIgnoredIssues; final boolean includeOpenedIssues; @@ -565,28 +561,31 @@ private Collection filterIgnoredIssues(Collection issueList) return issueList.stream() .filter(it -> it.isVisible(includeIgnoredIssues, includeOpenedIssues)) - .toList(); - + .collect(Collectors.toCollection(ArrayList::new)); } - - private Set populateIssueCache(PublishDiagnostics316Param param, String filePath) { + + private void populateIssueCache(PublishDiagnostics316Param param, String filePath) { var issueCache = getIssueCache(filePath); Diagnostic316[] diagnostics = param.getDiagnostics(); if (diagnostics.length == 0) { issueCache.removeAllIssuesForPath(filePath); - return Set.of(); + return; } var source = diagnostics[0].getSource(); if (StringUtils.isEmpty(source)) { - return Set.of(); + return; } var snykProduct = LSP_SOURCE_TO_SCAN_PARAMS.get(source); List issueList = new ArrayList<>(); - + var isIgnoresEnabled = Preferences.getInstance().getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED); for (var diagnostic : diagnostics) { if (diagnostic.getData() == null) { continue; } + if(!isIgnoresEnabled && diagnostic.getData().isIgnored()) { + toggleIgnores(true); + isIgnoresEnabled = true; + } issueList.add(diagnostic.getData()); } @@ -601,7 +600,6 @@ private Set populateIssueCache(PublishDiagnostics316Param param issueCache.addIacIssues(filePath, issueList); break; } - return getAffectedProductNodes(snykProduct, filePath); } public void reportAnalytics(AbstractTask event) { diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueComparator.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueComparator.java new file mode 100644 index 00000000..7bac6392 --- /dev/null +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueComparator.java @@ -0,0 +1,77 @@ +package io.snyk.languageserver.protocolextension.messageObjects.scanResults; + +import java.util.*; +import io.snyk.eclipse.plugin.domain.ProductConstants; + +public class IssueComparator implements Comparator { + + private final Map> issuesGrouped; + + public IssueComparator() { + this.issuesGrouped = new HashMap<>(); + } + + public IssueComparator(Collection issues) { + this.issuesGrouped = new HashMap<>(); + for (Issue issue : issues) { + this.issuesGrouped.computeIfAbsent(issue, k -> new ArrayList<>()).add(issue); + } + } + + @Override + public int compare(Issue o1, Issue o2) { + Map severityOrder = getSeverityOrderHashMap(); + + // Get ranks for the severities of the two issues + int rank1 = severityOrder.getOrDefault(o1.severity().toLowerCase(), Integer.MAX_VALUE); + int rank2 = severityOrder.getOrDefault(o2.severity().toLowerCase(), Integer.MAX_VALUE); + + // Compare based on severity rank (lower rank = higher severity) + int severityComparison = Integer.compare(rank1, rank2); + if (severityComparison != 0) { + return severityComparison; + } + + // Fallback: Compare by issue counts grouped by severity (cascading) + int o1Criticals = getCount(o1, ProductConstants.SEVERITY_CRITICAL); + int o2Criticals = getCount(o2, ProductConstants.SEVERITY_CRITICAL); + + int o1Highs = getCount(o1, ProductConstants.SEVERITY_HIGH); + int o2Highs = getCount(o2, ProductConstants.SEVERITY_HIGH); + + int o1Mediums = getCount(o1, ProductConstants.SEVERITY_MEDIUM); + int o2Mediums = getCount(o2, ProductConstants.SEVERITY_MEDIUM); + + int o1Lows = getCount(o1, ProductConstants.SEVERITY_LOW); + int o2Lows = getCount(o2, ProductConstants.SEVERITY_LOW); + + if (o1Criticals != o2Criticals) { + return Integer.compare(o2Criticals, o1Criticals); + } else if (o1Highs != o2Highs) { + return Integer.compare(o2Highs, o1Highs); + } else if (o1Mediums != o2Mediums) { + return Integer.compare(o2Mediums, o1Mediums); + } else if (o1Lows != o2Lows) { + return Integer.compare(o2Lows, o1Lows); + } + + // Fallback to comparing by hash codes if everything else is equal + return Integer.compare(o1.hashCode(), o2.hashCode()); + } + + private int getCount(Issue issue, String severity) { + List issuesForType = issuesGrouped.getOrDefault(issue, Collections.emptyList()); + return (int) issuesForType.stream() + .filter(i -> severity.equalsIgnoreCase(i.severity())) + .count(); + } + + private static Map getSeverityOrderHashMap() { + Map severityOrder = new HashMap<>(); + severityOrder.put(ProductConstants.SEVERITY_CRITICAL.toLowerCase(), 1); + severityOrder.put(ProductConstants.SEVERITY_HIGH.toLowerCase(), 2); + severityOrder.put(ProductConstants.SEVERITY_MEDIUM.toLowerCase(), 3); + severityOrder.put(ProductConstants.SEVERITY_LOW.toLowerCase(), 4); + return severityOrder; + } +} diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueSorter.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueSorter.java deleted file mode 100644 index 511d0153..00000000 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/scanResults/IssueSorter.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.snyk.languageserver.protocolextension.messageObjects.scanResults; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.snyk.eclipse.plugin.domain.ProductConstants; - -public class IssueSorter { - public static List sortIssuesBySeverity(Collection issues) { - List result = new ArrayList<>(issues); - Map severityOrder = getSeverityOrderHashMap(); - result.sort((i1, i2) -> { - Integer rank1 = severityOrder.getOrDefault(i1.severity().toLowerCase(), Integer.MAX_VALUE); - Integer rank2 = severityOrder.getOrDefault(i2.severity().toLowerCase(), Integer.MAX_VALUE); - return rank1.compareTo(rank2); - }); - return result; - } - - private static Map getSeverityOrderHashMap() { - Map severityOrder = new HashMap<>(); - severityOrder.put(ProductConstants.SEVERITY_CRITICAL, 1); - severityOrder.put(ProductConstants.SEVERITY_HIGH, 2); - severityOrder.put(ProductConstants.SEVERITY_MEDIUM, 3); - severityOrder.put(ProductConstants.SEVERITY_LOW, 4); - return severityOrder; - } -} diff --git a/tests/src/test/java/io/snyk/eclipse/plugin/html/HtmlProviderFactoryTest.java b/tests/src/test/java/io/snyk/eclipse/plugin/html/HtmlProviderFactoryTest.java new file mode 100644 index 00000000..5880503c --- /dev/null +++ b/tests/src/test/java/io/snyk/eclipse/plugin/html/HtmlProviderFactoryTest.java @@ -0,0 +1,54 @@ +package io.snyk.eclipse.plugin.html; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import io.snyk.eclipse.plugin.domain.ProductConstants; +import io.snyk.languageserver.LsBaseTest; + +public class HtmlProviderFactoryTest extends LsBaseTest { + @Test + void htmlProviderFactoryReturnsCorrectType() throws Exception { + var cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_CODE_SECURITY); + assertTrue(cut instanceof CodeHtmlProvider); + cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_CODE_QUALITY); + assertTrue(cut instanceof CodeHtmlProvider); + cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_OSS); + assertTrue(cut instanceof OssHtmlProvider); + cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_IAC); + assertTrue(cut instanceof IacHtmlProvider); + } + + @Test + void testHtmlProviderReplacesPlaceholders() throws Exception { + var htmlContent = Files.readString(Path.of("src/test/resources/code_issue_description.html")); + + var cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_CODE_SECURITY); + htmlContent = cut.replaceCssVariables(htmlContent); + assertTrue(!htmlContent.contains("${headerEnd})")); + assertTrue(!htmlContent.contains("${nonce})")); + assertTrue(!htmlContent.contains("${ideScript})")); + assertTrue(!htmlContent.contains("ideNonce")); + assertTrue(!htmlContent.contains("var(--text-color)")); + assertTrue(!htmlContent.contains("var(----background-color)")); + assertTrue(!htmlContent.contains("var(--border-color)")); + assertTrue(!htmlContent.contains("var(--link-color)")); + assertTrue(!htmlContent.contains("var(--text-color)")); + assertTrue(!htmlContent.contains("var(--horizontal-border-color)")); + assertTrue(!htmlContent.contains("var(--code-background-color)")); + assertTrue(!htmlContent.contains("var(--example-line-removed-color)")); + assertTrue(!htmlContent.contains("var(--example-line-added-color)")); + } + + @Test + void testHtmlProviderGeneratesInitScript() throws Exception { + var cut = HtmlProviderFactory.GetHtmlProvider(ProductConstants.DISPLAYED_CODE_SECURITY); + var initScript = cut.getInitScript(); + assertTrue(initScript.contains("window.openInEditor")); + assertTrue(initScript.contains("document.body.classList.add(isHighContrast")); + } +} diff --git a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java index c3cb5efe..bc6ab3e6 100644 --- a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java +++ b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java @@ -11,14 +11,11 @@ import static io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView.CONGRATS_NO_ISSUES_FOUND; import static io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView.getPlural; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -29,7 +26,6 @@ import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -51,12 +47,8 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; -import io.snyk.eclipse.plugin.analytics.AbstractTask; -import io.snyk.eclipse.plugin.analytics.AnalyticsEventTask; import io.snyk.eclipse.plugin.analytics.TaskProcessor; -import io.snyk.eclipse.plugin.properties.preferences.InMemoryPreferenceStore; import io.snyk.eclipse.plugin.properties.preferences.Preferences; -import io.snyk.eclipse.plugin.properties.preferences.PreferencesUtils; import io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView; import io.snyk.eclipse.plugin.views.snyktoolview.InfoTreeNode; import io.snyk.eclipse.plugin.views.snyktoolview.ProductTreeNode; diff --git a/tests/src/test/resources/code_issue_description.html b/tests/src/test/resources/code_issue_description.html new file mode 100644 index 00000000..24d1d321 --- /dev/null +++ b/tests/src/test/resources/code_issue_description.html @@ -0,0 +1,1411 @@ + + + + + + + + + + + + + ${ideStyle} + + + + +

+ + +
+
+ + + + Ignored. This issue is currently ignored. +
+
+ +
+ + + + +

NoSQL Injection

+
+
+
Issue
+ + | + + CWE-943 + + + + + | +
Position line: 39
+ + + | +
Priority score: 818
+ +
IGNORED
+
+ + + +
+ +
+ +
+
+ + + Ignore Details + + + + Fix Analysis + + + + Issue Overview + + +
+ + +
+ + +
+
+ +
+
Category
+
Ignored temporarily
+
+ +
+
Expiration
+
27 days
+
+ +
+
Ignored On
+
November 14, 2024
+
+ +
+
Ignored By
+
Abdelrahman Shawki
+
+ +
+
Reason
+
test test test
+
+ +
+ Ignores are currently managed in the Snyk web app. + To edit or remove the ignore please go to: + https://app.snyk.io. +
+
+
+ + + + +
+
+
Unsanitized input from the HTTP request body flows into find, where it is used in an NoSQL query. This may result in an NoSQL Injection vulnerability.
+ +
+ + + + + +
+ +
+

+ Data Flow - 1 step +

+
+ +
+ + index.js +
+ + + + + + + + + + + +
1 + line:39 + | + User.find({ username: req.body.username, password: req.body.password }, function (err, users) { +
+ +
+
+ + +
+

Fixed Code Examples

+ +

+ This type of vulnerability was fixed in 30 open source projects. +

+
+
+ + + + + + afuh/pinstagram + + +
+
+ + + + + + + + + + Example 1/3 + + + + + + + + + + +
+
+ +
+ +
+
+ +
+ const user = await User.findOne({ slug: req.params.user }).populate('likes') + +
+ +
+ const user = await User.findOne({ _id: req.user._id }).populate('likes') + +
+ +
+
+ + + + + +
+ +
+
+
+ + + +
+ +
+
+

Details

+ +

In an NoSQL injection attack, the user can submit an NoSQL query directly to the database, gaining access without providing appropriate credentials. Attackers can then view, export, modify, and delete confidential information; change passwords and other authentication information; and possibly gain access to other systems within the network. This is one of the most commonly exploited categories of vulnerability, but can largely be avoided through good coding practices.

+ +
+
+
+ +
+
+
+ + + + + + + ${ideScript} + + +