diff --git a/build.gradle b/build.gradle index ea63ac03..4718bf9f 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ repositories { } def buildInfoVersion = '2.41.6' -def idePluginsCommonVersion = '2.3.2' +def idePluginsCommonVersion = '2.3.3' dependencies { implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.15.2' diff --git a/gradle.properties b/gradle.properties index 4b1f698a..c7062abe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -webviewVersion=0.2.8 +webviewVersion=0.2.12 sandboxVersion=2022.3.2 -webviewChecksum=6b0bb07998ee6d83037f0278e61a210d22c5234f0906892ed5df4831fd5f7ff1 +webviewChecksum=cb88f9fd7fe2380a811c7b290a5aa451b972cdcba84fc9c41e5e274706c79a12 currentVersion=2.6.x-SNAPSHOT diff --git a/src/main/java/com/jfrog/ide/idea/scan/MavenScanner.java b/src/main/java/com/jfrog/ide/idea/scan/MavenScanner.java index cec06342..0bdd0fcb 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/MavenScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/MavenScanner.java @@ -9,6 +9,9 @@ import com.intellij.psi.search.GlobalSearchScope; import com.jfrog.ide.common.deptree.DepTree; import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.DescriptorFileTreeNode; +import com.jfrog.ide.common.nodes.FileTreeNode; import com.jfrog.ide.common.scan.ComponentPrefix; import com.jfrog.ide.common.scan.ScanLogic; import com.jfrog.ide.idea.inspections.AbstractInspection; @@ -28,6 +31,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; @@ -158,4 +162,60 @@ private void updateChildrenNodes(Map nodes, DepTreeNode par .forEach(childrenArtifactNode -> updateChildrenNodes(nodes, currentNode, childrenArtifactNode, false)); parentNode.getChildren().add(compId); } + + /** + * Groups a collection of {@link DependencyNode}s by the descriptor files of the modules that depend on them. + * The returned DependencyNodes inside the {@link FileTreeNode}s are clones of the ones in depScanResults. + * + * @param depScanResults collection of DependencyNodes + * @param depTree the project's dependency tree + * @param parents a map of components by their IDs and their parents in the dependency tree + * @return a list of FileTreeNodes (that are all DescriptorFileTreeNodes) having the DependencyNodes as their children + */ + @Override + protected List groupDependenciesToDescriptorNodes(Collection depScanResults, DepTree depTree, Map> parents) { + Map descriptorMap = new HashMap<>(); + Map> visitedComponents = new HashMap<>(); + for (DependencyNode dependencyNode : depScanResults) { + String vulnerableDepId = dependencyNode.getComponentIdWithoutPrefix(); + Set affectedModulesIds = getDependentModules(vulnerableDepId, depTree, parents, visitedComponents); + for (String descriptorId : affectedModulesIds) { + String descriptorPath = depTree.nodes().get(descriptorId).getDescriptorFilePath(); + descriptorMap.putIfAbsent(descriptorPath, new DescriptorFileTreeNode(descriptorPath)); + + // Each dependency might be a child of more than one POM file, but Artifact is a tree node, so it can have only one parent. + // The solution for this is to clone the dependency before adding it as a child of the POM. + DependencyNode clonedDep = (DependencyNode) dependencyNode.clone(); + clonedDep.setIndirect(!parents.get(vulnerableDepId).contains(descriptorId)); + descriptorMap.get(descriptorPath).addDependency(clonedDep); + } + } + return new CopyOnWriteArrayList<>(descriptorMap.values()); + } + + /** + * Retrieve component IDs of all modules in the project that are dependent on the specified component. + * + * @param compId the component ID to identify modules depending on it + * @param depTree the project's dependency tree + * @param parents a map of components by their IDs and their parents in the dependency tree + * @param visitedComponents a map of components for which dependent modules have already been found + * @return a set of component IDs representing modules dependent on the specified component + */ + Set getDependentModules(String compId, DepTree depTree, Map> parents, Map> visitedComponents) { + if (visitedComponents.containsKey(compId)) { + return visitedComponents.get(compId); + } + Set modulesIds = new HashSet<>(); + if (depTree.nodes().get(compId).getDescriptorFilePath() != null) { + modulesIds.add(compId); + } + if (parents.containsKey(compId)) { + for (String parentId : parents.get(compId)) { + modulesIds.addAll(getDependentModules(parentId, depTree, parents, visitedComponents)); + } + } + visitedComponents.put(compId, modulesIds); + return modulesIds; + } } diff --git a/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java index 7ca2d364..6a90a1c9 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java @@ -59,7 +59,7 @@ import static com.jfrog.ide.common.utils.Utils.createMapper; import static com.jfrog.ide.common.utils.Utils.createYAMLMapper; import static com.jfrog.ide.common.utils.XrayConnectionUtils.createXrayClientBuilder; -import static com.jfrog.ide.idea.scan.ScanUtils.getOSAndArc; +import static com.jfrog.ide.idea.scan.utils.ScanUtils.getOSAndArc; import static com.jfrog.ide.idea.utils.Utils.HOME_PATH; import static java.lang.String.join; diff --git a/src/main/java/com/jfrog/ide/idea/scan/ScannerBase.java b/src/main/java/com/jfrog/ide/idea/scan/ScannerBase.java index 0a52bc11..8f3c2c81 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ScannerBase.java +++ b/src/main/java/com/jfrog/ide/idea/scan/ScannerBase.java @@ -21,10 +21,7 @@ import com.jfrog.ide.common.deptree.DepTreeNode; import com.jfrog.ide.common.log.ProgressIndicator; import com.jfrog.ide.common.nodes.DependencyNode; -import com.jfrog.ide.common.nodes.DescriptorFileTreeNode; import com.jfrog.ide.common.nodes.FileTreeNode; -import com.jfrog.ide.common.nodes.subentities.ImpactTree; -import com.jfrog.ide.common.nodes.subentities.ImpactTreeNode; import com.jfrog.ide.common.scan.ComponentPrefix; import com.jfrog.ide.common.scan.ScanLogic; import com.jfrog.ide.idea.configuration.GlobalSettings; @@ -32,6 +29,7 @@ import com.jfrog.ide.idea.log.Logger; import com.jfrog.ide.idea.log.ProgressIndicatorImpl; import com.jfrog.ide.idea.scan.data.PackageManagerType; +import com.jfrog.ide.idea.scan.utils.ImpactTreeBuilder; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.LocalComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; @@ -50,7 +48,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -132,6 +129,18 @@ protected void sendUsageReport() { protected abstract PackageManagerType getPackageManagerType(); + /** + * Groups a collection of {@link DependencyNode}s by the descriptor files of the modules that depend on them. + * The returned DependencyNodes inside the {@link FileTreeNode}s might be clones of the ones in depScanResults, but + * it's not guaranteed. + * + * @param depScanResults collection of DependencyNodes + * @param depTree the project's dependency tree + * @param parents a map of components by their IDs and their parents in the dependency tree + * @return a list of FileTreeNodes (that are all DescriptorFileTreeNodes) having the DependencyNodes as their children + */ + protected abstract List groupDependenciesToDescriptorNodes(Collection depScanResults, DepTree depTree, Map> parents); + /** * Scan and update dependency components. * @@ -176,104 +185,29 @@ private void scanAndUpdate(ProgressIndicator indicator) { } } - /** - * Walks through a {@link DepTree}'s nodes. - * Builds impact paths for {@link DependencyNode} objects and groups them in {@link DescriptorFileTreeNode}s. - * - * @param vulnerableDependencies a map of component IDs and the DependencyNode object matching each of them. - * @param depTree the project's dependency tree to walk through. - */ protected List buildImpactGraph(Map vulnerableDependencies, DepTree depTree) throws IOException { - Map descriptorNodes = new HashMap<>(); - visitDepTreeNode(vulnerableDependencies, depTree, Collections.singletonList(depTree.rootId()), descriptorNodes, new ArrayList<>(), new HashMap<>()); - return new CopyOnWriteArrayList<>(descriptorNodes.values()); + Map> parents = getParents(depTree); + ImpactTreeBuilder.populateImpactTrees(vulnerableDependencies, parents, depTree.rootId()); + return groupDependenciesToDescriptorNodes(vulnerableDependencies.values(), depTree, parents); } /** - * Visit a node in the {@link DepTree} and walk through its children. - * Each impact path to a vulnerable dependency is added in its {@link DependencyNode}. - * Each DependencyNode is added to the relevant {@link DescriptorFileTreeNode}s. + * Find the parents of each node in the given {@link DepTree}. + * Nodes without parents (the root) don't appear in the returned map. * - * @param vulnerableDependencies a map of {@link DependencyNode}s by their component IDs. - * @param depTree the project's dependency tree. - * @param path a path of nodes (represented by their component IDs) from the root to the current node. - * @param descriptorNodes a map of {@link DescriptorFileTreeNode}s by the descriptor file path. Missing DescriptorFileTreeNodes will be added to this map. - * @param descriptorPaths a list of descriptor file paths that their matching components are in the path to the current node. - * @param addedDeps a map of all {@link DependencyNode}s already grouped to {@link DescriptorFileTreeNode}s. Newly grouped DependencyNodes will be added to this map. + * @param depTree the {@link DepTree} to find the parents of its nodes + * @return a map of nodes from the {@link DepTree} amd each one's parents */ - private void visitDepTreeNode(Map vulnerableDependencies, DepTree depTree, List path, - Map descriptorNodes, List descriptorPaths, - Map> addedDeps) { - String compId = path.get(path.size() - 1); - DepTreeNode compNode = depTree.nodes().get(compId); - List innerDescriptorPaths = descriptorPaths; - if (compNode.getDescriptorFilePath() != null) { - innerDescriptorPaths = new ArrayList<>(descriptorPaths); - innerDescriptorPaths.add(compNode.getDescriptorFilePath()); - } - if (vulnerableDependencies.containsKey(compId)) { - DependencyNode dependencyNode = vulnerableDependencies.get(compId); - addImpactPathToDependencyNode(dependencyNode, path); - - DepTreeNode parentCompNode = null; - if (path.size() >= 2) { - String parentCompId = path.get(path.size() - 2); - parentCompNode = depTree.nodes().get(parentCompId); - } - for (String descriptorPath : innerDescriptorPaths) { - boolean indirect = parentCompNode != null && !descriptorPath.equals(parentCompNode.getDescriptorFilePath()); - if (!descriptorNodes.containsKey(descriptorPath)) { - descriptorNodes.put(descriptorPath, new DescriptorFileTreeNode(descriptorPath)); - addedDeps.put(descriptorPath, new HashMap<>()); - } - DependencyNode existingDep = addedDeps.get(descriptorPath).get(compId); - if (existingDep != null) { - // If this dependency has any direct path, then it's direct - if (existingDep.isIndirect() && !indirect) { - existingDep.setIndirect(false); - } - continue; - } - // Each dependency might be a child of more than one descriptor, but DependencyNode is a tree node, so it can have only one parent. - // The solution for this is to clone the dependency before adding it as a child of the POM. - DependencyNode clonedDep = (DependencyNode) dependencyNode.clone(); - clonedDep.setIndirect(indirect); - - descriptorNodes.get(descriptorPath).addDependency(clonedDep); - addedDeps.get(descriptorPath).put(compId, clonedDep); - } - } - - for (String childId : compNode.getChildren()) { - List pathToChild = new ArrayList<>(path); - pathToChild.add(childId); - if (!path.contains(childId)) { - visitDepTreeNode(vulnerableDependencies, depTree, pathToChild, descriptorNodes, innerDescriptorPaths, addedDeps); - } - } - } - - protected void addImpactPathToDependencyNode(DependencyNode dependencyNode, List path) { - dependencyNode.setImpactTree(new ImpactTree(new ImpactTreeNode(path.get(0)))); - ImpactTree impactTree = dependencyNode.getImpactTree(); - if (impactTree.getImpactPathsCount() == ImpactTree.IMPACT_PATHS_LIMIT) { - return; - } - ImpactTreeNode parentImpactTreeNode = impactTree.getRoot(); - for (int pathNodeIndex = 1; pathNodeIndex < path.size(); pathNodeIndex++) { - String currPathNode = path.get(pathNodeIndex); - // Find a child of parentImpactTreeNode with a name equals to currPathNode - ImpactTreeNode currImpactTreeNode = parentImpactTreeNode.getChildren().stream().filter(impactTreeNode -> impactTreeNode.getName().equals(currPathNode)).findFirst().orElse(null); - if (currImpactTreeNode == null) { - currImpactTreeNode = new ImpactTreeNode(currPathNode); - parentImpactTreeNode.getChildren().add(currImpactTreeNode); - if (pathNodeIndex == path.size() - 1) { - // If a new leaf was added, thus a new impact path was added (impact paths don't collide after they split) - impactTree.incImpactPathsCount(); - } + static Map> getParents(DepTree depTree) { + Map> parents = new HashMap<>(); + for (Map.Entry node : depTree.nodes().entrySet()) { + String parentId = node.getKey(); + for (String childId : node.getValue().getChildren()) { + parents.putIfAbsent(childId, new HashSet<>()); + parents.get(childId).add(parentId); } - parentImpactTreeNode = currImpactTreeNode; } + return parents; } /** diff --git a/src/main/java/com/jfrog/ide/idea/scan/ScannerFactory.java b/src/main/java/com/jfrog/ide/idea/scan/ScannerFactory.java index 3fa120a1..449403b9 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ScannerFactory.java +++ b/src/main/java/com/jfrog/ide/idea/scan/ScannerFactory.java @@ -20,7 +20,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; -import static com.jfrog.ide.idea.scan.ScanUtils.createScanPaths; +import static com.jfrog.ide.idea.scan.utils.ScanUtils.createScanPaths; import static com.jfrog.ide.idea.utils.Utils.getProjectBasePath; /** diff --git a/src/main/java/com/jfrog/ide/idea/scan/SingleDescriptorScanner.java b/src/main/java/com/jfrog/ide/idea/scan/SingleDescriptorScanner.java index ddc617c2..2efa3138 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/SingleDescriptorScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/SingleDescriptorScanner.java @@ -1,12 +1,20 @@ package com.jfrog.ide.idea.scan; import com.intellij.openapi.project.Project; +import com.jfrog.ide.common.deptree.DepTree; +import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.DescriptorFileTreeNode; +import com.jfrog.ide.common.nodes.FileTreeNode; import com.jfrog.ide.common.scan.ComponentPrefix; import com.jfrog.ide.common.scan.ScanLogic; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; import org.jetbrains.annotations.NotNull; +import org.jfrog.build.extractor.scan.DependencyTree; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; public abstract class SingleDescriptorScanner extends ScannerBase { @@ -39,4 +47,31 @@ public abstract class SingleDescriptorScanner extends ScannerBase { ScanLogic scanLogic) { this(project, basePath, prefix, executor, "", scanLogic); } + + /** + * Groups a collection of {@link DependencyNode}s by the descriptor files of the modules that depend on them. + * The returned DependencyNodes inside the {@link FileTreeNode}s are references of the ones in depScanResults. + * + * @param depScanResults collection of DependencyNodes + * @param depTree the project's dependency tree + * @param parents a map of components by their IDs and their parents in the dependency tree + * @return a list of FileTreeNodes (that are all DescriptorFileTreeNodes) having the DependencyNodes as their children + */ + @Override + protected List groupDependenciesToDescriptorNodes(Collection depScanResults, DepTree depTree, Map> parents) { + DescriptorFileTreeNode fileTreeNode = new DescriptorFileTreeNode(descriptorFilePath); + for (DependencyNode dependency : depScanResults) { + boolean directDep = false; + for (String parentId : parents.get(dependency.getComponentIdWithoutPrefix())) { + DepTreeNode parent = depTree.nodes().get(parentId); + if (descriptorFilePath.equals(parent.getDescriptorFilePath())) { + directDep = true; + break; + } + } + dependency.setIndirect(!directDep); + fileTreeNode.addDependency(dependency); + } + return new CopyOnWriteArrayList<>(List.of(fileTreeNode)); + } } diff --git a/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java b/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java index dccac920..ad936bd8 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java @@ -16,6 +16,7 @@ import com.jfrog.ide.idea.inspections.AbstractInspection; import com.jfrog.ide.idea.inspections.YarnInspection; import com.jfrog.ide.idea.scan.data.PackageManagerType; +import com.jfrog.ide.idea.scan.utils.ImpactTreeBuilder; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; import org.apache.commons.collections4.CollectionUtils; @@ -102,7 +103,7 @@ private void buildImpactGraphFromPaths(DescriptorFileTreeNode descriptorNode, Ma // build the impact graph for each vulnerable dependency out of its impact paths for (List impactPath : impactPaths) { - this.addImpactPathToDependencyNode(dependencyNode, impactPath); + ImpactTreeBuilder.addImpactPathToDependencyNode(dependencyNode, impactPath); } boolean direct = impactPaths.stream().map(List::size).anyMatch(size -> size == 2); diff --git a/src/main/java/com/jfrog/ide/idea/scan/data/applications/JFrogApplicationsConfig.java b/src/main/java/com/jfrog/ide/idea/scan/data/applications/JFrogApplicationsConfig.java index dde08b0a..c02f23d9 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/data/applications/JFrogApplicationsConfig.java +++ b/src/main/java/com/jfrog/ide/idea/scan/data/applications/JFrogApplicationsConfig.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.intellij.openapi.project.Project; import com.jfrog.ide.idea.configuration.GlobalSettings; -import com.jfrog.ide.idea.scan.ScanUtils; +import com.jfrog.ide.idea.scan.utils.ScanUtils; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilder.java b/src/main/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilder.java new file mode 100644 index 00000000..2f3494ad --- /dev/null +++ b/src/main/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilder.java @@ -0,0 +1,78 @@ +package com.jfrog.ide.idea.scan.utils; + +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.DescriptorFileTreeNode; +import com.jfrog.ide.common.nodes.FileTreeNode; +import com.jfrog.ide.common.nodes.subentities.ImpactTree; +import com.jfrog.ide.common.nodes.subentities.ImpactTreeNode; + +import java.util.*; + +public class ImpactTreeBuilder { + /** + * Builds impact paths for {@link DependencyNode} objects. + * + * @param vulnerableDependencies a map of component IDs and the {@link DependencyNode} object matching each of them. + * Impact paths will be built for these DependencyNodes + * @param parents a map of all dependencies and their parents + * @param rootId the project's root component ID + */ + public static void populateImpactTrees(Map vulnerableDependencies, Map> parents, String rootId) { + for (DependencyNode vulnDep : vulnerableDependencies.values()) { + walkParents(vulnDep, parents, rootId, Collections.singletonList(vulnDep.getComponentIdWithoutPrefix())); + } + } + + /** + * Walks through a {@link DependencyNode}'s parents to build its impact paths. + * + * @param depNode a vulnerable dependency + * @param parents a map of all dependencies and their parents + * @param rootId the project's root component ID + * @param path a path of nodes (represented by their component IDs) from the current parent to the current node + */ + private static void walkParents(DependencyNode depNode, Map> parents, String rootId, List path) { + String currParentId = path.get(0); + if (depNode.getImpactTree() != null && depNode.getImpactTree().getImpactPathsCount() >= ImpactTree.IMPACT_PATHS_LIMIT) { + return; + } + // If we arrived at the root, add the path to the impact tree + if (currParentId.equals(rootId)) { + addImpactPathToDependencyNode(depNode, path); + } else { + for (String grandparentId : parents.get(currParentId)) { + if (path.contains(grandparentId)) { + continue; + } + List pathToGrandparent = new ArrayList<>(path); + pathToGrandparent.add(0, grandparentId); + walkParents(depNode, parents, rootId, pathToGrandparent); + } + } + } + + public static void addImpactPathToDependencyNode(DependencyNode dependencyNode, List path) { + if (dependencyNode.getImpactTree() == null) { + dependencyNode.setImpactTree(new ImpactTree(new ImpactTreeNode(path.get(0)))); + } + ImpactTree impactTree = dependencyNode.getImpactTree(); + if (impactTree.getImpactPathsCount() >= ImpactTree.IMPACT_PATHS_LIMIT) { + return; + } + ImpactTreeNode parentImpactTreeNode = impactTree.getRoot(); + for (int pathNodeIndex = 1; pathNodeIndex < path.size(); pathNodeIndex++) { + String currPathNode = path.get(pathNodeIndex); + // Find a child of parentImpactTreeNode with a name equals to currPathNode + ImpactTreeNode currImpactTreeNode = parentImpactTreeNode.getChildren().stream().filter(impactTreeNode -> impactTreeNode.getName().equals(currPathNode)).findFirst().orElse(null); + if (currImpactTreeNode == null) { + currImpactTreeNode = new ImpactTreeNode(currPathNode); + parentImpactTreeNode.getChildren().add(currImpactTreeNode); + if (pathNodeIndex == path.size() - 1) { + // If a new leaf was added, thus a new impact path was added (impact paths don't collide after they split) + impactTree.incImpactPathsCount(); + } + } + parentImpactTreeNode = currImpactTreeNode; + } + } +} diff --git a/src/main/java/com/jfrog/ide/idea/scan/ScanUtils.java b/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java similarity index 96% rename from src/main/java/com/jfrog/ide/idea/scan/ScanUtils.java rename to src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java index 5022cacb..48f08635 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ScanUtils.java +++ b/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java @@ -1,4 +1,4 @@ -package com.jfrog.ide.idea.scan; +package com.jfrog.ide.idea.scan.utils; import com.google.common.collect.Sets; import com.intellij.openapi.module.Module; @@ -39,7 +39,7 @@ public static Set createScanPaths(Project project) { return paths; } - static String getOSAndArc() throws IOException { + public static String getOSAndArc() throws IOException { String arch = SystemUtils.OS_ARCH; // Windows if (SystemUtils.IS_OS_WINDOWS) { diff --git a/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewObjectConverter.java b/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewObjectConverter.java index bc9d4f1e..a60b97e6 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewObjectConverter.java +++ b/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewObjectConverter.java @@ -3,6 +3,7 @@ import com.jfrog.ide.common.nodes.*; import com.jfrog.ide.common.nodes.subentities.*; import com.jfrog.ide.common.scan.ComponentPrefix; +import com.jfrog.ide.idea.scan.utils.ImpactTreeBuilder; import com.jfrog.ide.idea.ui.webview.model.Cve; import com.jfrog.ide.idea.ui.webview.model.Evidence; import com.jfrog.ide.idea.ui.webview.model.License; @@ -142,7 +143,7 @@ public static DependencyPage convertLicenseToDepPage(LicenseViolationNode licens } private static ImpactGraph convertImpactGraph(ImpactTree impactTree) { - return new ImpactGraph(convertImpactGraphNode(impactTree.getRoot()), impactTree.getImpactPathsCount(), ImpactTree.IMPACT_PATHS_LIMIT); + return new ImpactGraph(convertImpactGraphNode(impactTree.getRoot()), impactTree.getImpactPathsCount() >= ImpactTree.IMPACT_PATHS_LIMIT ? impactTree.getImpactPathsCount() : -1); } private static ImpactGraphNode convertImpactGraphNode(ImpactTreeNode impactTreeNode) { diff --git a/src/main/java/com/jfrog/ide/idea/ui/webview/model/ImpactGraph.java b/src/main/java/com/jfrog/ide/idea/ui/webview/model/ImpactGraph.java index 36acd2af..d7e76895 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/webview/model/ImpactGraph.java +++ b/src/main/java/com/jfrog/ide/idea/ui/webview/model/ImpactGraph.java @@ -2,12 +2,10 @@ public class ImpactGraph { private final ImpactGraphNode root; - private final int pathsCount; private final int pathsLimit; - public ImpactGraph(ImpactGraphNode root, int pathsCount, int pathsLimit) { + public ImpactGraph(ImpactGraphNode root, int pathsLimit) { this.root = root; - this.pathsCount = pathsCount; this.pathsLimit = pathsLimit; } @@ -16,11 +14,6 @@ public ImpactGraphNode getRoot() { return root; } - @SuppressWarnings("unused") - public int getPathsCount() { - return pathsCount; - } - @SuppressWarnings("unused") public int getPathsLimit() { return pathsLimit; diff --git a/src/test/java/com/jfrog/ide/idea/scan/MavenScannerTest.java b/src/test/java/com/jfrog/ide/idea/scan/MavenScannerTest.java new file mode 100644 index 00000000..c2947115 --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/scan/MavenScannerTest.java @@ -0,0 +1,73 @@ +package com.jfrog.ide.idea.scan; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import com.jfrog.ide.common.deptree.DepTree; +import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.FileTreeNode; +import com.jfrog.ide.common.nodes.LicenseViolationNode; +import com.jfrog.ide.common.nodes.subentities.Severity; +import com.jfrog.ide.idea.scan.utils.ImpactTreeBuilder; + +import java.util.*; + +public class MavenScannerTest extends BasePlatformTestCase { + public void testGroupDependenciesToDescriptorNodes() { + final String MULTI_DESC_PATH = "/path/to/project/maven-example/pom.xml"; + final String MULTI1_DESC_PATH = "/path/to/project/maven-example/multi1/pom.xml"; + final String MULTI2_DESC_PATH = "/path/to/project/maven-example/multi2/pom.xml"; + final String MULTI3_DESC_PATH = "/path/to/project/maven-example/multi3/pom.xml"; + + final String MULTI_COMP_ID = "org.jfrog.test:multi:3.7.x-SNAPSHOT"; + final String MULTI1_COMP_ID = "org.jfrog.test:multi1:3.7.x-SNAPSHOT"; + final String MULTI2_COMP_ID = "org.jfrog.test:multi2:3.7.x-SNAPSHOT"; + final String MULTI3_COMP_ID = "org.jfrog.test:multi3:3.7.x-SNAPSHOT"; + final String SPRING_AOP_COMP_ID = "org.springframework:spring-aop:2.5.6"; + final String SPRING_CORE_COMP_ID = "org.springframework:spring-core:2.5.6"; + final String LOG4J_COMP_ID = "log4j:log4j:1.2.17"; + + DepTree depTree = new DepTree(MULTI_COMP_ID, new HashMap<>() {{ + put(MULTI_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI_DESC_PATH).children(Set.of(MULTI1_COMP_ID, MULTI3_COMP_ID, LOG4J_COMP_ID))); + put(MULTI1_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI1_DESC_PATH).children(Set.of(LOG4J_COMP_ID))); + put(MULTI2_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI2_DESC_PATH).children(Set.of(SPRING_CORE_COMP_ID))); + put(MULTI3_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI3_DESC_PATH).children(Set.of(MULTI1_COMP_ID, LOG4J_COMP_ID, SPRING_AOP_COMP_ID))); + put(SPRING_AOP_COMP_ID, new DepTreeNode().children(Set.of(SPRING_CORE_COMP_ID))); + put(SPRING_CORE_COMP_ID, new DepTreeNode().children(Set.of())); + put(LOG4J_COMP_ID, new DepTreeNode().children(Set.of())); + }}); + Map> parents = ScannerBase.getParents(depTree); + + List vulnerableDeps = new ArrayList<>() {{ + add(new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("gav://" + LOG4J_COMP_ID)); + add(new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("gav://" + SPRING_AOP_COMP_ID)); + }}; + + MavenScanner scanner = new MavenScanner(getProject(), null, null); + List fileTreeNodes = scanner.groupDependenciesToDescriptorNodes(vulnerableDeps, depTree, parents); + assertEquals(3, fileTreeNodes.size()); + + FileTreeNode multiDescFile = fileTreeNodes.stream().filter(fileTreeNode -> fileTreeNode.getFilePath().equals(MULTI_DESC_PATH)).findFirst().get(); + FileTreeNode multi1DescFile = fileTreeNodes.stream().filter(fileTreeNode -> fileTreeNode.getFilePath().equals(MULTI1_DESC_PATH)).findFirst().get(); + FileTreeNode multi3DescFile = fileTreeNodes.stream().filter(fileTreeNode -> fileTreeNode.getFilePath().equals(MULTI3_DESC_PATH)).findFirst().get(); + + assertEquals(2, multiDescFile.getChildren().size()); + DependencyNode multiLog4jDepNode = multiDescFile.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).filter(depNode -> depNode.getComponentId().equals("gav://" + LOG4J_COMP_ID)).findFirst().get(); + assertFalse(multiLog4jDepNode.isIndirect()); + DependencyNode multiSpringAopDepNode = multiDescFile.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).filter(depNode -> depNode.getComponentId().equals("gav://" + SPRING_AOP_COMP_ID)).findFirst().get(); + assertTrue(multiSpringAopDepNode.isIndirect()); + + assertEquals(1, multi1DescFile.getChildren().size()); + DependencyNode multi1Log4jDepNode = multi1DescFile.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).filter(depNode -> depNode.getComponentId().equals("gav://" + LOG4J_COMP_ID)).findFirst().get(); + assertFalse(multi1Log4jDepNode.isIndirect()); + + assertEquals(2, multi3DescFile.getChildren().size()); + DependencyNode multi3Log4jDepNode = multi3DescFile.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).filter(depNode -> depNode.getComponentId().equals("gav://" + LOG4J_COMP_ID)).findFirst().get(); + assertFalse(multi3Log4jDepNode.isIndirect()); + DependencyNode multi3SpringAopDepNode = multi3DescFile.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).filter(depNode -> depNode.getComponentId().equals("gav://" + SPRING_AOP_COMP_ID)).findFirst().get(); + assertFalse(multi3SpringAopDepNode.isIndirect()); + } +} diff --git a/src/test/java/com/jfrog/ide/idea/scan/ScannerBaseTest.java b/src/test/java/com/jfrog/ide/idea/scan/ScannerBaseTest.java new file mode 100644 index 00000000..8b6fcba6 --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/scan/ScannerBaseTest.java @@ -0,0 +1,54 @@ +package com.jfrog.ide.idea.scan; + +import com.jfrog.ide.common.deptree.DepTree; +import com.jfrog.ide.common.deptree.DepTreeNode; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class ScannerBaseTest { + @Test + public void testGetParents() { + final String MULTI_DESC_PATH = "/path/to/project/maven-example/pom.xml"; + final String MULTI1_DESC_PATH = "/path/to/project/maven-example/multi1/pom.xml"; + final String MULTI3_DESC_PATH = "/path/to/project/maven-example/multi3/pom.xml"; + + final String MULTI_COMP_ID = "org.jfrog.test:multi:3.7.x-SNAPSHOT"; + final String MULTI1_COMP_ID = "org.jfrog.test:multi1:3.7.x-SNAPSHOT"; + final String MULTI3_COMP_ID = "org.jfrog.test:multi3:3.7.x-SNAPSHOT"; + final String SPRING_AOP_COMP_ID = "org.springframework:spring-aop:2.5.6"; + final String LOG4J_COMP_ID = "log4j:log4j:1.2.17"; + + DepTree depTree = new DepTree(MULTI_COMP_ID, new HashMap<>() {{ + put(MULTI_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI_DESC_PATH).children(Set.of(MULTI1_COMP_ID, MULTI3_COMP_ID, LOG4J_COMP_ID))); + put(MULTI1_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI1_DESC_PATH).children(Set.of(LOG4J_COMP_ID))); + put(MULTI3_COMP_ID, new DepTreeNode().descriptorFilePath(MULTI3_DESC_PATH).children(Set.of(MULTI1_COMP_ID, LOG4J_COMP_ID, SPRING_AOP_COMP_ID))); + put(SPRING_AOP_COMP_ID, new DepTreeNode().children(Set.of())); + put(LOG4J_COMP_ID, new DepTreeNode().children(Set.of())); + }}); + + Map> expectedParents = new HashMap<>() {{ + put(MULTI1_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + add(MULTI3_COMP_ID); + }}); + put(MULTI3_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + }}); + put(SPRING_AOP_COMP_ID, new HashSet<>() {{ + add(MULTI3_COMP_ID); + }}); + put(LOG4J_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + add(MULTI1_COMP_ID); + add(MULTI3_COMP_ID); + }}); + }}; + Map> actualParents = ScannerBase.getParents(depTree); + Assert.assertEquals(expectedParents, actualParents); + } +} diff --git a/src/test/java/com/jfrog/ide/idea/scan/SingleDescriptorScannerTest.java b/src/test/java/com/jfrog/ide/idea/scan/SingleDescriptorScannerTest.java new file mode 100644 index 00000000..2b403726 --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/scan/SingleDescriptorScannerTest.java @@ -0,0 +1,57 @@ +package com.jfrog.ide.idea.scan; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import com.jfrog.ide.common.deptree.DepTree; +import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.FileTreeNode; +import com.jfrog.ide.common.nodes.LicenseViolationNode; +import com.jfrog.ide.common.nodes.subentities.Severity; +import org.junit.Test; + +import java.nio.file.Paths; +import java.util.*; + +public class SingleDescriptorScannerTest extends BasePlatformTestCase { + public void testGroupDependenciesToDescriptorNodes() { + final String ROOT_ID = "npm-example:0.0.3"; + final String ROOT_PATH = Paths.get("path", "to", "project").toString(); + final String SEND_COMP_ID = "send:0.5.0"; + final String DEBUG_COMP_ID = "debug:1.0.2"; + final String MS_COMP_ID = "ms:0.6.2"; + + DepTree depTree = new DepTree(ROOT_ID, new HashMap<>() {{ + put(ROOT_ID, new DepTreeNode().descriptorFilePath(Paths.get(ROOT_PATH, "package.json").toString()).children(Set.of(SEND_COMP_ID, MS_COMP_ID))); + put(SEND_COMP_ID, new DepTreeNode().children(Set.of(DEBUG_COMP_ID, MS_COMP_ID))); + put(DEBUG_COMP_ID, new DepTreeNode().children(Set.of())); + put(MS_COMP_ID, new DepTreeNode().children(Set.of())); + }}); + Map> parents = ScannerBase.getParents(depTree); + List vulnerableDeps = new ArrayList<>() {{ + add(new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("npm://" + SEND_COMP_ID)); + add(new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("npm://" + DEBUG_COMP_ID)); + add(new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("npm://" + MS_COMP_ID)); + }}; + + NpmScanner scanner = new NpmScanner(getProject(), ROOT_PATH, null, null); + List fileTreeNodes = scanner.groupDependenciesToDescriptorNodes(vulnerableDeps, depTree, parents); + assertEquals(1, fileTreeNodes.size()); + + FileTreeNode descriptorNode = fileTreeNodes.get(0); + List depNodes = descriptorNode.getChildren().stream().map(treeNode -> (DependencyNode) treeNode).toList(); + assertEquals(3, descriptorNode.getChildren().size()); + + DependencyNode sendDepNode = depNodes.stream().filter(depNode -> depNode.getComponentId().equals("npm://" + SEND_COMP_ID)).findFirst().get(); + assertFalse(sendDepNode.isIndirect()); + DependencyNode debugDepNode = depNodes.stream().filter(depNode -> depNode.getComponentId().equals("npm://" + DEBUG_COMP_ID)).findFirst().get(); + assertTrue(debugDepNode.isIndirect()); + DependencyNode msDepNode = depNodes.stream().filter(depNode -> depNode.getComponentId().equals("npm://" + MS_COMP_ID)).findFirst().get(); + assertFalse(msDepNode.isIndirect()); + } +} diff --git a/src/test/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilderTest.java b/src/test/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilderTest.java new file mode 100644 index 00000000..24f3161d --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/scan/utils/ImpactTreeBuilderTest.java @@ -0,0 +1,104 @@ +package com.jfrog.ide.idea.scan.utils; + +import com.jfrog.ide.common.deptree.DepTree; +import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.nodes.DependencyNode; +import com.jfrog.ide.common.nodes.FileTreeNode; +import com.jfrog.ide.common.nodes.LicenseViolationNode; +import com.jfrog.ide.common.nodes.subentities.ImpactTree; +import com.jfrog.ide.common.nodes.subentities.ImpactTreeNode; +import com.jfrog.ide.common.nodes.subentities.Severity; +import org.junit.Assert; +import org.junit.Test; + +import java.util.*; + +public class ImpactTreeBuilderTest { + private final String MULTI_DESC_PATH = "/path/to/project/maven-example/pom.xml"; + private final String MULTI1_DESC_PATH = "/path/to/project/maven-example/multi1/pom.xml"; + private final String MULTI3_DESC_PATH = "/path/to/project/maven-example/multi3/pom.xml"; + + private final String MULTI_COMP_ID = "org.jfrog.test:multi:3.7.x-SNAPSHOT"; + private final String MULTI1_COMP_ID = "org.jfrog.test:multi1:3.7.x-SNAPSHOT"; + private final String MULTI3_COMP_ID = "org.jfrog.test:multi3:3.7.x-SNAPSHOT"; + private final String SPRING_AOP_COMP_ID = "org.springframework:spring-aop:2.5.6"; + private final String SPRING_CORE_COMP_ID = "org.springframework:spring-core:2.5.6"; + private final String LOG4J_COMP_ID = "log4j:log4j:1.2.17"; + + private Map> parents = new HashMap<>() {{ + put(MULTI1_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + add(MULTI3_COMP_ID); + }}); + put(MULTI3_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + }}); + put(SPRING_AOP_COMP_ID, new HashSet<>() {{ + add(MULTI3_COMP_ID); + }}); + put(SPRING_CORE_COMP_ID, new HashSet<>() {{ + add(SPRING_AOP_COMP_ID); + }}); + put(LOG4J_COMP_ID, new HashSet<>() {{ + add(MULTI_COMP_ID); + add(MULTI1_COMP_ID); + add(MULTI3_COMP_ID); + }}); + }}; + private ImpactTree log4jImpactTree = new ImpactTree(new ImpactTreeNode(MULTI_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(LOG4J_COMP_ID)); + getChildren().add(new ImpactTreeNode(MULTI1_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(LOG4J_COMP_ID)); + }}); + getChildren().add(new ImpactTreeNode(MULTI3_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(LOG4J_COMP_ID)); + getChildren().add(new ImpactTreeNode(MULTI1_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(LOG4J_COMP_ID)); + }}); + }}); + }}); + private ImpactTree springAopImpactTree = new ImpactTree(new ImpactTreeNode(MULTI_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(MULTI3_COMP_ID) {{ + getChildren().add(new ImpactTreeNode(SPRING_AOP_COMP_ID)); + }}); + }}); + + @Test + public void testBuildImpactTrees() { + Map vulnerableDeps = new HashMap<>() {{ + put(LOG4J_COMP_ID, new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("gav://" + LOG4J_COMP_ID)); + put(SPRING_AOP_COMP_ID, new DependencyNode() {{ + addIssue(new LicenseViolationNode("", "", null, Severity.High, "", null)); + }}.componentId("gav://" + SPRING_AOP_COMP_ID)); + }}; + ImpactTreeBuilder.populateImpactTrees(vulnerableDeps, parents, MULTI_COMP_ID); + + assertImpactTree(log4jImpactTree, vulnerableDeps.get(LOG4J_COMP_ID).getImpactTree()); + assertImpactTree(springAopImpactTree, vulnerableDeps.get(SPRING_AOP_COMP_ID).getImpactTree()); + } + + @Test + public void testAddImpactPathToDependencyNode() { + DependencyNode depNode = new DependencyNode(); + ImpactTreeBuilder.addImpactPathToDependencyNode(depNode, List.of(MULTI_COMP_ID, LOG4J_COMP_ID)); + ImpactTreeBuilder.addImpactPathToDependencyNode(depNode, List.of(MULTI_COMP_ID, MULTI1_COMP_ID, LOG4J_COMP_ID)); + ImpactTreeBuilder.addImpactPathToDependencyNode(depNode, List.of(MULTI_COMP_ID, MULTI3_COMP_ID, LOG4J_COMP_ID)); + ImpactTreeBuilder.addImpactPathToDependencyNode(depNode, List.of(MULTI_COMP_ID, MULTI3_COMP_ID, MULTI1_COMP_ID, LOG4J_COMP_ID)); + assertImpactTree(log4jImpactTree, depNode.getImpactTree()); + } + + private static void assertImpactTree(ImpactTree expected, ImpactTree actual) { + assertImpactTreeNode(expected.getRoot(), actual.getRoot()); + } + + private static void assertImpactTreeNode(ImpactTreeNode expected, ImpactTreeNode actual) { + Assert.assertEquals(expected.getName(), actual.getName()); + Assert.assertEquals(expected.getChildren().size(), actual.getChildren().size()); + expected.getChildren().forEach(expectedNode -> { + ImpactTreeNode actualNode = actual.getChildren().stream().filter(actualImpactTreeNode -> actualImpactTreeNode.getName().equals(expectedNode.getName())).findFirst().get(); + assertImpactTreeNode(actualNode, expectedNode); + }); + } +}