Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/main/java/com/jfrog/ide/common/deptree/DepTree.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package com.jfrog.ide.common.deptree;

import java.util.List;
import java.util.Map;

/**
* Represents a dependency tree.
*
* @param rootId The root node ID
* @param nodes A map of the nodes in the tree by their component IDs
* @param rootId The root node ID
* @param nodes A map of the nodes in the tree by their component IDs, merged across all modules
* @param modules The per-module trees, for projects whose modules may resolve a component differently.
* Empty for ecosystems that don't report per-module dependency trees.
*/
public record DepTree(String rootId, Map<String, DepTreeNode> nodes) {
public record DepTree(String rootId, Map<String, DepTreeNode> nodes, List<DepTreeModule> modules) {

public DepTree(String rootId, Map<String, DepTreeNode> nodes) {
this(rootId, nodes, List.of());
}

public DepTreeNode getRootNode() {
return nodes.get(rootId);
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/jfrog/ide/common/deptree/DepTreeModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.jfrog.ide.common.deptree;

import java.util.Map;

/**
* The dependency tree of a single module of a multi-module project, as resolved by that module.
*
* @param rootId the module's root node ID
* @param nodes the nodes reachable from the module's root, by their component IDs
*/
public record DepTreeModule(String rootId, Map<String, DepTreeNode> nodes) {
}
59 changes: 42 additions & 17 deletions src/main/java/com/jfrog/ide/common/gradle/GradleTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@
import com.jfrog.GradleDepTreeResults;
import com.jfrog.GradleDependencyNode;
import com.jfrog.ide.common.deptree.DepTree;
import com.jfrog.ide.common.deptree.DepTreeModule;
import com.jfrog.ide.common.deptree.DepTreeNode;
import org.jfrog.build.api.util.Log;
import org.jfrog.build.extractor.scan.GeneralInfo;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Build Gradle dependency tree before the Xray scan.
Expand Down Expand Up @@ -56,27 +61,47 @@ public DepTree buildTree(Log logger) throws IOException {
* @throws IOException in case of any I/O error.
*/
private DepTree createDependencyTrees(List<File> gradleDependenciesFiles) throws IOException {
List<DepTreeModule> modules = new ArrayList<>();
for (File moduleDepsFile : gradleDependenciesFiles) {
modules.add(readModule(moduleDepsFile));
}
Map<String, DepTreeNode> nodes = mergeModules(modules);
Set<String> moduleRootIds = modules.stream().map(DepTreeModule::rootId).collect(Collectors.toCollection(HashSet::new));
if (moduleRootIds.size() == 1) {
return new DepTree(moduleRootIds.iterator().next(), nodes, modules);
}
String rootId = projectDir.getFileName().toString();
DepTreeNode rootNode = new DepTreeNode().descriptorFilePath(descriptorFilePath);
nodes.put(rootId, new DepTreeNode().descriptorFilePath(descriptorFilePath).children(moduleRootIds));
return new DepTree(rootId, nodes, modules);
}

private DepTreeModule readModule(File moduleDepsFile) throws IOException {
GradleDepTreeResults results = objectMapper.readValue(moduleDepsFile, GradleDepTreeResults.class);
Map<String, DepTreeNode> nodes = new HashMap<>();
for (File moduleDepsFile : gradleDependenciesFiles) {
GradleDepTreeResults results = objectMapper.readValue(moduleDepsFile, GradleDepTreeResults.class);
for (Map.Entry<String, GradleDependencyNode> nodeEntry : results.getNodes().entrySet()) {
String compId = nodeEntry.getKey();
GradleDependencyNode gradleDep = nodeEntry.getValue();
DepTreeNode node = new DepTreeNode().scopes(gradleDep.getConfigurations()).children(gradleDep.getChildren());
nodes.put(compId, node);
}
String moduleRootId = results.getRoot();
nodes.get(moduleRootId).descriptorFilePath(descriptorFilePath);
rootNode.getChildren().add(moduleRootId);
}
if (rootNode.getChildren().size() == 1) {
return new DepTree(rootNode.getChildren().iterator().next(), nodes);
results.getNodes().forEach((compId, gradleDep) -> nodes.put(compId, new DepTreeNode()
.scopes(new HashSet<>(gradleDep.getConfigurations()))
.children(new HashSet<>(gradleDep.getChildren()))));
nodes.get(results.getRoot()).descriptorFilePath(descriptorFilePath);
return new DepTreeModule(results.getRoot(), nodes);
}

/**
* A component shared by several modules may be resolved with different configurations and transitive
* dependencies in each, so its nodes are merged rather than replaced.
*/
private Map<String, DepTreeNode> mergeModules(List<DepTreeModule> modules) {
Map<String, DepTreeNode> merged = new HashMap<>();
for (DepTreeModule module : modules) {
module.nodes().forEach((compId, moduleNode) -> {
DepTreeNode node = merged.computeIfAbsent(compId, id -> new DepTreeNode());
node.getScopes().addAll(moduleNode.getScopes());
Comment thread
Jordanh1996 marked this conversation as resolved.
node.getChildren().addAll(moduleNode.getChildren());
if (moduleNode.getDescriptorFilePath() != null) {
node.descriptorFilePath(moduleNode.getDescriptorFilePath());
}
});
}
nodes.put(rootId, rootNode);
return new DepTree(rootId, nodes);
return merged;
}

private GeneralInfo createGeneralInfo(String id, GradleDependencyNode node) {
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/com/jfrog/ide/common/parse/SarifParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,17 @@ private List<FileTreeNode> parseScanFindings(List<Run> runs){
List<Result> resultsList = run.getResults();
// get the scanner tool name with characters only
String sourceCodeToolName = run.getTool().getDriver().getName().replaceAll("[^a-zA-Z\\s]", "").trim();
SourceCodeScanType reporter = SourceCodeScanType.fromParam(sourceCodeToolName);
SourceCodeScanType reporter;
try {
reporter = SourceCodeScanType.fromParam(sourceCodeToolName);
} catch (IllegalArgumentException e) {
Comment thread
Jordanh1996 marked this conversation as resolved.
if (resultsList == null || resultsList.isEmpty()) {
log.debug("Skipping an unsupported scanner with no results: " + sourceCodeToolName);
} else {
log.warn("Skipping " + resultsList.size() + " results of an unsupported scanner: " + sourceCodeToolName);
}
continue;
}
Comment thread
Jordanh1996 marked this conversation as resolved.

for (Result result : resultsList){
ReportingDescriptor rule = run.getTool().getDriver().getRules().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.jfrog.ide.common.nodes.subentities.Severity;
import com.jfrog.ide.common.nodes.subentities.SourceCodeScanType;
import com.jfrog.ide.common.parse.SarifParser;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.jfrog.build.api.util.NullLog;
import org.jfrog.build.extractor.executor.CommandResults;
Expand Down Expand Up @@ -46,6 +47,7 @@ public class JfrogCliDriverTest {
private final String XRAY_URL = SERVER_URL + "xray/";
private String testServerId;
private File tempDir;
private File exampleProjectCopy;
private final SarifParser parser = new SarifParser(new NullLog());
private final Logger logger = LoggerFactory.getLogger(JfrogCliDriverTest.class);

Expand Down Expand Up @@ -204,8 +206,8 @@ public void testAddServerConfig_withBadCredentials() {
@Test
public void testRunAudit_NpmProject() {
try {
Path exampleProjectsFolder = Path.of("src/test/resources/example-projects/npm");
CommandResults response = jfrogCliDriver.runCliAudit(exampleProjectsFolder.toFile(),
File exampleProject = copyExampleProject("npm");
CommandResults response = jfrogCliDriver.runCliAudit(exampleProject,
null, testServerId, testEnv);
assertEquals(response.getExitValue(),0);
logger.info("Audit debug logs: \n" + response.getErr());
Expand All @@ -214,8 +216,8 @@ public void testRunAudit_NpmProject() {
assertNotNull(findings);
assertFalse(findings.isEmpty(), "Expected findings in SARIF output for npm project");
// Verify the findings
assertEquals(findings.size(), 1, "Expected exactly one file with findings");
FileTreeNode node = findings.get(0);
FileTreeNode node = findings.stream().filter(finding -> finding.getTitle().equals("package.json")).findFirst().orElse(null);
assertNotNull(node, "Expected SCA findings in package.json");
assertEquals(node.getChildren().size(), 1, "Expected exactly one vulnerabilities");
FileIssueNode issue = (FileIssueNode) node.getChildren().get(0);
assertEquals(issue.getSeverity(), Severity.High, "Expected severity to be HIGH");
Expand All @@ -229,8 +231,8 @@ public void testRunAudit_NpmProject() {
public void testRunAudit_MultiMavenProject() {
List<String> projectsToCheck = new ArrayList<>(Arrays.asList("multi1", "multi2"));
try {
Path exampleProjectsFolder = Path.of("src/test/resources/example-projects/maven-example");
CommandResults response = jfrogCliDriver.runCliAudit(exampleProjectsFolder.toFile(),
File exampleProject = copyExampleProject("maven-example");
Comment thread
Jordanh1996 marked this conversation as resolved.
CommandResults response = jfrogCliDriver.runCliAudit(exampleProject,
projectsToCheck, testServerId, testEnv);
assertEquals(response.getExitValue(), 0);
logger.info("Audit debug logs: \n" + response.getErr());
Expand All @@ -239,8 +241,8 @@ public void testRunAudit_MultiMavenProject() {
assertNotNull(findings);
assertFalse(findings.isEmpty(), "Expected findings in SARIF output for multi-maven project");
// Verify the findings
assertEquals(findings.size(), 1, "Expected exactly one file with findings");
FileTreeNode node = findings.get(0);
FileTreeNode node = findings.stream().filter(finding -> finding.getTitle().equals("pom.xml")).findFirst().orElse(null);
assertNotNull(node, "Expected SCA findings in pom.xml");
assertEquals(node.getChildren().size(), 3, "Expected exactly three vulnerabilities");
assertEquals(node.getSeverity(), Severity.High, "Expected severity to be HIGH");
FileIssueNode issue = (FileIssueNode) node.getChildren().get(0);
Expand All @@ -250,30 +252,40 @@ public void testRunAudit_MultiMavenProject() {
}
}

private String createServerId() {
/**
* Audits run on a copy of the example project, because jf audit skips a working directory whose absolute
* path matches its default exclusion patterns, and this repository keeps its fixtures under src/test.
*/
private File copyExampleProject(String projectName) throws IOException {
exampleProjectCopy = Files.createTempDirectory("ide-plugins-common-audit").toFile();
FileUtils.copyDirectory(Path.of("src/test/resources/example-projects", projectName).toFile(), exampleProjectCopy);
return exampleProjectCopy;
}

private String createServerId() {
return "ide-plugins-common-test-server-" + timeStampFormat.format(System.currentTimeMillis());
}

@Test
public void testRunAudit_WithExcludedPattern() {
try {
Path exampleProjectsFolder = Path.of("src/test/resources/example-projects/maven-example");
File exampleProject = copyExampleProject("maven-example");
AuditConfig config = new AuditConfig.Builder()
.serverId(testServerId)
.excludedPattern(new ArrayList<>(List.of("*multi3*")))
.serverId(testServerId)
.envVars(testEnv)
.build();
CommandResults response = jfrogCliDriver.runCliAudit(exampleProjectsFolder.toFile(), config);
CommandResults response = jfrogCliDriver.runCliAudit(exampleProject, config);
assertEquals(response.getExitValue(), 0);
logger.info("Audit debug logs: \n" + response.getErr());
logger.info("Audit response: \n" + response.getRes());
List<FileTreeNode> findings = parser.parse(response.getRes());
assertNotNull(findings);
assertFalse(findings.isEmpty(), "Expected findings in SARIF output for multi-maven project");
// Verify the findings
assertEquals(findings.size(), 1, "Expected exactly one file with findings");
FileTreeNode node = findings.get(0);
FileTreeNode node = findings.stream().filter(finding -> finding.getTitle().equals("pom.xml")).findFirst().orElse(null);
assertNotNull(node, "Expected SCA findings in pom.xml");
assertEquals(node.getChildren().size(), 3, "Expected exactly three vulnerabilities");
assertEquals(node.getSeverity(), Severity.High, "Expected severity to be HIGH");
FileIssueNode issue = (FileIssueNode) node.getChildren().get(0);
Expand All @@ -285,6 +297,7 @@ public void testRunAudit_WithExcludedPattern() {

@AfterMethod
public void cleanUp(Method method) {
FileUtils.deleteQuietly(exampleProjectCopy);
try {
if (!TEST_NAME_TO_SKIP_CLI_DOWNLOAD.equals(method.getName())) {
String[] serverConfigCmdArgs = {"config", "remove", testServerId, "--quiet"};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.jfrog.GradleDependencyNode;
import com.jfrog.ide.common.TestUtils;
import com.jfrog.ide.common.deptree.DepTree;
import com.jfrog.ide.common.deptree.DepTreeModule;
import com.jfrog.ide.common.deptree.DepTreeNode;
import org.apache.commons.io.FileUtils;
import org.jfrog.build.api.util.NullLog;
Expand Down Expand Up @@ -80,6 +81,45 @@ public void gradleTreeBuilderUnresolvedTest(String projectPath) throws IOExcepti
assertTrue(missing.getScopes().contains("testImplementation"));
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One-row data provider plus javadoc that only returns 'sharedDependency' is noise. A plain @Test fits this fixture.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data provider is required. @BeforeMethod setUp(Object[] args) copies the fixture named in args[0], so a plain @Test would reach setUp with no arguments. That's why every test in this class uses one.

* Data provider for a project whose modules resolve the same dependency with different transitive
* dependencies - 'modb' excludes 'commons-lang3' from 'commons-text', 'moda' doesn't.
*
* @return 'sharedDependency'.
*/
@DataProvider
private Object[][] gradleTreeBuilderSharedDependencyProvider() {
return new Object[][]{{"sharedDependency"}};
}

@SuppressWarnings("unused")
@Test(dataProvider = "gradleTreeBuilderSharedDependencyProvider")
public void gradleTreeBuilderSharedDependencyTest(String projectPath) throws IOException {
final String COMMONS_TEXT = "org.apache.commons:commons-text:1.9";
final String COMMONS_LANG3 = "org.apache.commons:commons-lang3:3.11";
DepTree depTree = buildGradleDependencyTree(projectPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildGradleDependencyTree asserts getRootNode().getChildren().size() == 3 (meant for groovy/kotlin). This fixture only passes that because the init script still emits the root project plus moda/modb.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the 3 isn't a coincidence: this fixture has exactly the root project plus moda and modb, and each one writes a dependency-tree file. The helper's assertion holds for this fixture for the same reason it does for groovy/kotlin, so I'd leave it.


DepTreeNode commonsText = depTree.nodes().get(COMMONS_TEXT);
assertNotNull(commonsText, "Couldn't find node '" + COMMONS_TEXT + "'.");
assertTrue(commonsText.getChildren().contains(COMMONS_LANG3),
"The merged tree must keep the edge resolved by 'moda'");

Map<String, DepTreeModule> modulesByRoot = new HashMap<>();
depTree.modules().forEach(module -> modulesByRoot.put(module.rootId(), module));
DepTreeModule moda = modulesByRoot.get("org.jfrog.test.gradle.shared:moda:1.0-SNAPSHOT");
assertNotNull(moda, "Couldn't find the 'moda' module tree in " + modulesByRoot.keySet());
DepTreeNode modaCommonsText = moda.nodes().get(COMMONS_TEXT);
assertNotNull(modaCommonsText, "Couldn't find '" + COMMONS_TEXT + "' in the 'moda' module tree.");
assertTrue(modaCommonsText.getChildren().contains(COMMONS_LANG3));
assertTrue(modaCommonsText.getScopes().contains("implementation"));
DepTreeModule modb = modulesByRoot.get("org.jfrog.test.gradle.shared:modb:1.0-SNAPSHOT");
assertNotNull(modb, "Couldn't find the 'modb' module tree in " + modulesByRoot.keySet());
DepTreeNode modbCommonsText = modb.nodes().get(COMMONS_TEXT);
assertNotNull(modbCommonsText, "Couldn't find '" + COMMONS_TEXT + "' in the 'modb' module tree.");
assertFalse(modbCommonsText.getChildren().contains(COMMONS_LANG3),
"'modb' excludes commons-lang3, so its own tree must not contain the edge");
}

private DepTree buildGradleDependencyTree(String projectPath) throws IOException {
// Add path to gradle-dep-tree JAR to "pluginLibDir" environment variable, to be read in gradle-dep-tree.gradle init script
Map<String, String> env = new HashMap<>(System.getenv());
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/com/jfrog/ide/common/parse/SarifParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public void testParseInvalidSarifReport() {
assertThrows(NullPointerException.class, () -> parser.parse(readSarifReportFromFile(resourcesDir + "invalid_sarif_no_results.json")));
}

@Test
public void testParseSarifReportSkipsUnsupportedScanners() throws IOException {
results = parser.parse(readSarifReportFromFile(resourcesDir + "sca_and_unsupported_scanners.json"));

assertEquals(results.size(), 1);
assertEquals(results.get(0).getChildren().size(), 10);
}

@Test
public void testParseSarifReportWithOnlyScaResults() throws IOException {
results = parser.parse(readSarifReportFromFile(resourcesDir + "sca_no_jas.json"));
Expand Down
25 changes: 25 additions & 0 deletions src/test/resources/gradle/sharedDependency/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
allprojects {
group = 'org.jfrog.test.gradle.shared'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
}

subprojects {
apply plugin: 'java'
}

project('moda') {
dependencies {
implementation 'org.apache.commons:commons-text:1.9'
}
}

project('modb') {
dependencies {
implementation('org.apache.commons:commons-text:1.9') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
public class Moda {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
public class Modb {
}
1 change: 1 addition & 0 deletions src/test/resources/gradle/sharedDependency/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include "moda", "modb"
Loading
Loading