diff --git a/bundle/META-INF/MANIFEST.MF b/bundle/META-INF/MANIFEST.MF index 0ade212..6d890b7 100644 --- a/bundle/META-INF/MANIFEST.MF +++ b/bundle/META-INF/MANIFEST.MF @@ -30,4 +30,5 @@ Export-Package: com.jfrog.ide.eclipse.configuration, com.jfrog.ide.eclipse.ui, com.jfrog.ide.eclipse.ui.actions, com.jfrog.ide.eclipse.ui.issues, + com.jfrog.ide.eclipse.ui.webview, com.jfrog.ide.eclipse.utils diff --git a/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverter.java b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverter.java new file mode 100644 index 0000000..45d6b9f --- /dev/null +++ b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverter.java @@ -0,0 +1,150 @@ +package com.jfrog.ide.eclipse.ui.webview; + +import com.jfrog.ide.common.nodes.*; +import com.jfrog.ide.common.nodes.subentities.*; +import com.jfrog.ide.common.parse.Applicability; +import com.jfrog.ide.common.webview.ApplicableDetails; +import com.jfrog.ide.common.webview.Cve; +import com.jfrog.ide.common.webview.DependencyPage; +import com.jfrog.ide.common.webview.ImpactGraph; +import com.jfrog.ide.common.webview.ImpactGraphNode; +import com.jfrog.ide.common.webview.IssuePage; +import com.jfrog.ide.common.webview.Location; +import com.jfrog.ide.common.webview.SastIssuePage; + +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +public class WebviewObjectConverter { + public static final int IMPACT_PATHS_LIMIT = 20; + public static DependencyPage convertScaIssueToDepPage(ScaIssueNode scaNode) { + return new DependencyPage() + .cve(new Cve(scaNode.getTitle(), null, null, null, null, new ApplicableDetails(Applicability.getWebviewIconName(scaNode.getApplicability()), null, null))) + .component(scaNode.getComponentName()) + .version(scaNode.getComponentVersion()) + .severity(scaNode.getSeverity().getSeverityName()) + .fixedVersion(scaNode.getFixedVersions()) + .summary(scaNode.getFullDescription()) + .impactGraph(toImpactGraph(scaNode.getImpactPaths())); + } + + public static IssuePage convertFileIssueToIssuePage(FileIssueNode fileIssueNode) { + return new IssuePage() + .header(fileIssueNode.getTitle()) + .type(ConvertPageType(fileIssueNode.getReporterType())) + .severity(fileIssueNode.getSeverity().name()) + .description(fileIssueNode.getFullDescription()) + .location(convertFileLocation(fileIssueNode)); + } + + public static SastIssuePage convertSastIssueToSastIssuePage(SastIssueNode sastIssueNode) { + return new SastIssuePage(convertFileIssueToIssuePage(sastIssueNode)) + .setAnalysisSteps(convertCodeFlowsToLocations(sastIssueNode.getCodeFlows())) + .setRuleID(sastIssueNode.getRuleId()); + } + + private static Location[] convertCodeFlowsToLocations(FindingInfo[][] codeFlows) { + if (codeFlows != null && codeFlows.length > 0) { + FindingInfo[] codeFlowList = codeFlows[0]; + int codeFlowListSize = codeFlowList.length; + Location[] locations = new Location[codeFlowListSize]; + for (int i = 0; i < codeFlowListSize; i++) { + FindingInfo codeFlow = codeFlows[0][i]; + locations[i] = new Location( + codeFlow.getFilePath(), + Paths.get(codeFlow.getFilePath()).getFileName().toString(), + codeFlow.getRowStart(), + codeFlow.getColStart(), + codeFlow.getRowEnd(), + codeFlow.getColEnd(), + codeFlow.getLineSnippet()); + } + return locations; + } + return null; + } + + private static String ConvertPageType(SourceCodeScanType reporterType) { + return switch (reporterType) { + case SECRETS -> "SECRETS"; + case IAC -> "IAC"; + case SAST -> "SAST"; + default -> "EMPTY"; // any other value passed will result the load of the default WebView page + }; + } + + private static Location convertFileLocation(FileIssueNode fileIssueNodeNode) { + return new Location( + fileIssueNodeNode.getFilePath(), + Paths.get(fileIssueNodeNode.getFilePath()).getFileName().toString(), + fileIssueNodeNode.getRowStart() + 1, + fileIssueNodeNode.getColStart() + 1, + fileIssueNodeNode.getRowEnd() + 1, + fileIssueNodeNode.getColEnd() + 1, + fileIssueNodeNode.getLineSnippet()); + } + + /** + * Converts a list of impact paths to an ImpactGraph. + * Each path is a list of ImpactPath objects, representing a path from root to leaf. + * Node names are "name:version" (or just "name" if version is empty). + */ + public static ImpactGraph toImpactGraph(List> impactPaths) { + if (impactPaths == null || impactPaths.isEmpty()) { + return new ImpactGraph(new ImpactGraphNode("", new ImpactGraphNode[0]), 0); + } + // Use the first element in each path as the root for that path + Map rootMap = new LinkedHashMap<>(); + boolean isMaxLimitExceeded; + int pathsNumber = impactPaths.size(); + int pathIndex = 0; + + for (; pathIndex < pathsNumber && pathIndex < IMPACT_PATHS_LIMIT; pathIndex++) { + List currentPath = impactPaths.get(pathIndex); + if (currentPath == null || currentPath.isEmpty()) { + continue; + } + + String rootName = getNodeName(currentPath.get(0)); + ImpactTreeNode root = rootMap.computeIfAbsent(rootName, ImpactTreeNode::new); + ImpactTreeNode currentNode = root; + int currentPathSize = currentPath.size(); + + for (int nodeIndex = 1; nodeIndex < currentPathSize; nodeIndex++) { + String nodeName = getNodeName(currentPath.get(nodeIndex)); + currentNode = getOrAddChild(currentNode, nodeName); + } + } + + isMaxLimitExceeded = pathIndex >= IMPACT_PATHS_LIMIT ? true : false; + + ImpactGraphNode rootGraphNode = toImpactGraphNode(rootMap.values().iterator().next()); + // pass value for pathsLimit only if exceeded the defined IMPACT_PATHS_LIMIT, so a corresponding message will appear in the WebView UI + return new ImpactGraph(rootGraphNode, isMaxLimitExceeded ? IMPACT_PATHS_LIMIT : -1); + } + + private static String getNodeName(ImpactPath ip) { + return ip.getName() + (ip.getVersion() != null && !ip.getVersion().isEmpty() ? ":" + ip.getVersion() : ""); + } + + // Helper to find or add a child node by name + private static ImpactTreeNode getOrAddChild(ImpactTreeNode parent, String nodeName) { + for (ImpactTreeNode child : parent.getChildren()) { + if (child.getName().equals(nodeName)) { + return child; + } + } + ImpactTreeNode newChild = new ImpactTreeNode(nodeName); + parent.getChildren().add(newChild); + return newChild; + } + + // Convert ImpactTreeNode to ImpactGraphNode tree + private static ImpactGraphNode toImpactGraphNode(ImpactTreeNode impactTreeNode) { + ImpactGraphNode[] children = impactTreeNode.getChildren().stream().map(WebviewObjectConverter::toImpactGraphNode).toArray(ImpactGraphNode[]::new); + return new ImpactGraphNode(impactTreeNode.getName(), children); + } +} diff --git a/tests/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverterTest.java b/tests/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverterTest.java new file mode 100644 index 0000000..fda80e6 --- /dev/null +++ b/tests/src/main/java/com/jfrog/ide/eclipse/ui/webview/WebviewObjectConverterTest.java @@ -0,0 +1,180 @@ +package com.jfrog.ide.eclipse.ui.webview; + +import com.jfrog.ide.common.nodes.FileIssueNode; +import com.jfrog.ide.common.nodes.ScaIssueNode; +import com.jfrog.ide.common.nodes.SastIssueNode; +import com.jfrog.ide.common.nodes.subentities.ImpactPath; +import com.jfrog.ide.common.nodes.subentities.Severity; +import com.jfrog.ide.common.nodes.subentities.SourceCodeScanType; +import com.jfrog.ide.common.parse.Applicability; +import com.jfrog.ide.common.webview.*; +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +public class WebviewObjectConverterTest extends TestCase{ + + private ScaIssueNode scaIssueNode; + private FileIssueNode secretIssueNode; + private SastIssueNode sastIssueNode; + + // setup common data + String filePath = "/test/path/file.java"; + int rowStart = 10; + int colStart = 5; + int rowEnd = 20; + int colEnd = 10; + String lineSnippet = "vulnerable code line"; + + public void testConvertScaIssueToDepPage() { + // Setup SCA test data + String scaTitle = "CVE-2023-1234"; + String reason = "sca issue reason"; + Severity severity = Severity.High; + String ruleID = "CVE-2023-1234_test-component_1.0.0"; + Applicability applicability = Applicability.APPLICABLE; + List> impactPaths = createTestImpactPaths(); + String[] fixedVersions = {"[1.0.1]", "[1.0.2]"}; + String fullDescription = "Test vulnerability description"; + + scaIssueNode = new ScaIssueNode(scaTitle, reason, severity, ruleID, applicability, impactPaths, fixedVersions, fullDescription); + DependencyPage result = WebviewObjectConverter.convertScaIssueToDepPage(scaIssueNode); + + assertNotNull(result); + assertEquals(scaIssueNode.getComponentName(), result.getComponent()); + assertEquals(scaIssueNode.getComponentVersion(), result.getVersion()); + assertEquals(scaIssueNode.getSeverity().getSeverityName(), result.getSeverity()); + assertEquals(scaIssueNode.getFullDescription(), result.getSummary()); + assertEquals(scaIssueNode.getFixedVersions(), result.getFixedVersion()); + assertNotNull(result.getCve()); + assertEquals(scaIssueNode.getTitle(), result.getCve().getId()); + } + + public void testConvertFileIssueToIssuePage() { + // setup secrets test data + String secretTitle = "Secret issue"; + String secretReason = "Hard coded secrets were found"; + Severity secretSeverity = Severity.Medium; + String secretRuleId = "SECRET-RULE"; + String secretFullDescription = "Test Secret issue description"; + + secretIssueNode = new FileIssueNode(secretTitle, filePath, rowStart, colStart, rowEnd, colEnd, secretReason, lineSnippet, SourceCodeScanType.SECRETS, secretSeverity, secretRuleId, secretFullDescription); + IssuePage result = WebviewObjectConverter.convertFileIssueToIssuePage(secretIssueNode); + + assertNotNull(result); + assertEquals(secretIssueNode.getTitle(), result.getHeader()); + assertEquals(secretIssueNode.getSeverity().name(), result.getSeverity()); + assertEquals(secretIssueNode.getFullDescription(), result.getDescription()); + assertNotNull(result.getLocation()); + assertEquals(secretIssueNode.getFilePath(), result.getLocation().getFile()); + assertEquals(secretIssueNode.getRowStart() + 1, result.getLocation().getStartRow()); + assertEquals(secretIssueNode.getColStart() + 1, result.getLocation().getStartColumn()); + } + + public void testConvertSastIssueToSastIssuePage() { + // setup SAST test data + String sastTitle = "SAST Issue"; + String sastReason = "SAST issue reason"; + Severity sastSeverity = Severity.Critical; + String sastRuleId = "SAST-RULE"; + String sastFullDescription = "Test SAST issue description"; + + sastIssueNode = new SastIssueNode(sastTitle, filePath, rowStart, colStart, rowEnd, colEnd, sastReason, lineSnippet, null, sastSeverity, sastRuleId, sastFullDescription); + + SastIssuePage result = WebviewObjectConverter.convertSastIssueToSastIssuePage(sastIssueNode); + + assertNotNull(result); + assertEquals(sastIssueNode.getTitle(), result.getHeader()); + assertEquals(sastIssueNode.getRuleId(), result.getRuleId()); + assertEquals(sastIssueNode.getSeverity().name(), result.getSeverity()); + assertEquals(sastIssueNode.getFullDescription(), result.getDescription()); + } + + public void testToImpactGraph_EmptyInput() { + ImpactGraph result = WebviewObjectConverter.toImpactGraph(null); + + assertNotNull(result); + assertNotNull(result.getRoot()); + assertEquals("", result.getRoot().getName()); + assertEquals(0, result.getRoot().getChildren().length); + } + + public void testToImpactGraph_SinglePath() { + List> impactPaths = new ArrayList<>(); + List path = new ArrayList<>(); + path.add(new ImpactPath("root", "1.0")); + path.add(new ImpactPath("child1", "2.0")); + path.add(new ImpactPath("child2", "3.0")); + impactPaths.add(path); + + ImpactGraph result = WebviewObjectConverter.toImpactGraph(impactPaths); + + assertNotNull(result); + assertNotNull(result.getRoot()); + assertEquals("root:1.0", result.getRoot().getName()); + assertEquals(1, result.getRoot().getChildren().length); + assertEquals("child1:2.0", result.getRoot().getChildren()[0].getName()); + assertEquals(1, result.getRoot().getChildren()[0].getChildren().length); + assertEquals("child2:3.0", result.getRoot().getChildren()[0].getChildren()[0].getName()); + // validate IMPACT_PATHS_LIMIT wasn't exceeded + assertEquals(-1, result.getPathsLimit()); + } + + public void testToImpactGraph_MultiplePaths() { + List> impactPaths = new ArrayList<>(); + + // First path + List path1 = new ArrayList<>(); + path1.add(new ImpactPath("root", "1.0")); + path1.add(new ImpactPath("child1", "2.0")); + impactPaths.add(path1); + + // Second path + List path2 = new ArrayList<>(); + path2.add(new ImpactPath("root", "1.0")); + path2.add(new ImpactPath("child2", "3.0")); + impactPaths.add(path2); + + ImpactGraph result = WebviewObjectConverter.toImpactGraph(impactPaths); + + assertNotNull(result); + assertNotNull(result.getRoot()); + assertEquals("root:1.0", result.getRoot().getName()); + assertEquals(2, result.getRoot().getChildren().length); + + // Verify both children exist + boolean hasChild1 = false; + boolean hasChild2 = false; + for (ImpactGraphNode child : result.getRoot().getChildren()) { + if (child.getName().equals("child1:2.0")) hasChild1 = true; + if (child.getName().equals("child2:3.0")) hasChild2 = true; + } + assertTrue(hasChild1 && hasChild2); + } + + public void testToImpactGraph_ExceedsLimit() { + List> impactPaths = new ArrayList<>(); + for (int i = 0; i < WebviewObjectConverter.IMPACT_PATHS_LIMIT + 5; i++) { + List path = new ArrayList<>(); + path.add(new ImpactPath("root" + i, "1.0")); + path.add(new ImpactPath("child" + i, "2.0")); + impactPaths.add(path); + } + + ImpactGraph result = WebviewObjectConverter.toImpactGraph(impactPaths); + + assertNotNull(result); + assertEquals(WebviewObjectConverter.IMPACT_PATHS_LIMIT, result.getPathsLimit()); + } + + private List> createTestImpactPaths() { + List> impactPaths = new ArrayList<>(); + List path = new ArrayList<>(); + path.add(new ImpactPath("root", "1.0")); + path.add(new ImpactPath("child1", "2.0")); + path.add(new ImpactPath("child2", "3.0")); + impactPaths.add(path); + return impactPaths; + } +} \ No newline at end of file