Skip to content

Commit

Permalink
Merge pull request #2568 from walterdeboer/feature/2567
Browse files Browse the repository at this point in the history
New option to only return outdated components and/or only direct dependenncies in the ComponentResource
  • Loading branch information
nscuro authored Jun 28, 2023
2 parents d73b236 + b58d2cc commit a7e5122
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,19 @@
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;

import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonValue;
import java.io.StringReader;
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.HashSet;
import javax.json.Json;
import javax.json.JsonValue;
import javax.json.JsonArray;

final class ComponentQueryManager extends QueryManager implements IQueryManager {

Expand Down Expand Up @@ -133,17 +132,49 @@ public List<Component> getAllComponents(Project project) {
/**
* Returns a List of Dependency for the specified Project.
* @param project the Project to retrieve dependencies of
* @param includeMetrics Optionally includes third-party metadata about the component from external repositories
* @return a List of Dependency objects
*/
public PaginatedResult getComponents(final Project project, final boolean includeMetrics) {
return getComponents(project, includeMetrics, false, false);
}
/**
* Returns a List of Dependency for the specified Project.
* @param project the Project to retrieve dependencies of
* @param includeMetrics Optionally includes third-party metadata about the component from external repositories
* @param onlyOutdated Optionally exclude recent components so only outdated components are shown
* @param onlyDirect Optionally exclude transitive dependencies so only direct dependencies are shown
* @return a List of Dependency objects
*/
public PaginatedResult getComponents(final Project project, final boolean includeMetrics, final boolean onlyOutdated, final boolean onlyDirect) {
final PaginatedResult result;
final Query<Component> query = pm.newQuery(Component.class, "project == :project");
String querySring ="SELECT FROM org.dependencytrack.model.Component WHERE project == :project ";
if (filter != null) {
querySring += " && (project == :project) && name.toLowerCase().matches(:name)";
}
if (onlyOutdated) {
// Components are considered outdated when metadata does exists, but the version is different than latestVersion
// Different should always mean version < latestVersion
// Hack JDO using % instead of .* to get the SQL LIKE clause working:
querySring +=
" && !("+
" SELECT FROM org.dependencytrack.model.RepositoryMetaComponent m " +
" WHERE m.name == this.name " +
" && m.namespace == this.group " +
" && m.latestVersion != this.version " +
" && this.purl.matches('pkg:' + m.repositoryType.toString().toLowerCase() + '/%') " +
" ).isEmpty()";
}
if (onlyDirect) {
querySring +=
" && this.project.directDependencies.matches('%\"uuid\":\"'+this.uuid+'\"%') "; // only direct dependencies
}
final Query<Component> query = pm.newQuery(querySring);
query.getFetchPlan().setMaxFetchDepth(2);
if (orderBy == null) {
query.setOrdering("name asc, version desc");
}
if (filter != null) {
query.setFilter("project == :project && name.toLowerCase().matches(:name)");
final String filterString = ".*" + filter.toLowerCase() + ".*";
result = execute(query, project, filterString);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,10 @@ public PaginatedResult getComponents(final Project project, final boolean includ
return getComponentQueryManager().getComponents(project, includeMetrics);
}

public PaginatedResult getComponents(final Project project, final boolean includeMetrics, final boolean onlyOutdated, final boolean onlyDirect) {
return getComponentQueryManager().getComponents(project, includeMetrics, onlyOutdated, onlyDirect);
}

public ServiceComponent matchServiceIdentity(final Project project, final ComponentIdentity cid) {
return getServiceComponentQueryManager().matchServiceIdentity(project, cid);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,18 @@ public class ComponentResource extends AlpineResource {
@ApiResponse(code = 404, message = "The project could not be found")
})
@PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
public Response getAllComponents(@PathParam("uuid") String uuid) {
public Response getAllComponents(
@ApiParam(value = "The UUID of the project to retrieve components for", required = true)
@PathParam("uuid") String uuid,
@ApiParam(value = "Optionally exclude recent components so only outdated components are returned", required = false)
@QueryParam("onlyOutdated") boolean onlyOutdated,
@ApiParam(value = "Optionally exclude transitive dependencies so only direct dependencies are returned", required = false)
@QueryParam("onlyDirect") boolean onlyDirect) {
try (QueryManager qm = new QueryManager(getAlpineRequest())) {
final Project project = qm.getObjectByUuid(Project.class, uuid);
if (project != null) {
if (qm.hasAccess(super.getPrincipal(), project)) {
final PaginatedResult result = qm.getComponents(project, true);
final PaginatedResult result = qm.getComponents(project, true, onlyOutdated, onlyDirect);
return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build();
} else {
return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,15 @@
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;

import static org.assertj.core.api.Assertions.assertThat;

public class PolicyEngineTest extends PersistenceCapableTest {
Expand All @@ -76,6 +75,11 @@ public static void tearDownClass() {
NotificationService.getInstance().unsubscribe(new Subscription(NotificationSubscriber.class));
}

@Before
public void setup() {
NOTIFICATIONS.clear();
}

@After
public void tearDown() {
NOTIFICATIONS.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import alpine.common.util.UuidUtil;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import org.apache.http.HttpStatus;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.Component;
Expand All @@ -33,15 +35,15 @@
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.junit.Assert;
import org.junit.Test;

import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

public class ComponentResourceTest extends ResourceTest {
Expand All @@ -63,28 +65,123 @@ public void getComponentsDefaultRequestTest() {
Assert.assertEquals(405, response.getStatus()); // No longer prohibited in DT 4.0+
}

@Test
public void getAllComponentsTest() {
/**
* Generate a project with different dependencies
* @return A project with 1000 dpendencies: <ul>
* <li>200 outdated dependencies, 75 direct and 125 transitive</li>
* <li>800 recent dependencies, 25 direct, 775 transitive</li>
* @throws MalformedPackageURLException
*/
private Project prepareProject() throws MalformedPackageURLException {
final Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);
final List<String> directDepencencies = new ArrayList<>();
// Generate 1000 dependencies
for (int i = 0; i < 1000; i++) {
Component component = new Component();
component.setProject(project);
component.setName("Component Name");
component.setVersion(String.valueOf(i));
qm.createComponent(component, false);
component.setGroup("component-group");
component.setName("component-name-"+i);
component.setVersion(String.valueOf(i)+".0");
component.setPurl(new PackageURL(RepositoryType.MAVEN.toString(), "component-group", "component-name-"+i , String.valueOf(i)+".0", null, null));
component = qm.createComponent(component, false);
// direct depencencies
if (i < 100) {
// 100 direct depencencies, 900 transitive depencencies
directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}");
}
// Recent & Outdated
if ((i >= 25) && (i < 225)) {
// 100 outdated components, 75 of these are direct dependencies, 25 transitive
final var metaComponent = new RepositoryMetaComponent();
metaComponent.setRepositoryType(RepositoryType.MAVEN);
metaComponent.setNamespace("component-group");
metaComponent.setName("component-name-"+i);
metaComponent.setLatestVersion(String.valueOf(i+1)+".0");
metaComponent.setLastCheck(new Date());
qm.persist(metaComponent);
} else if (i<500) {
// 300 recent components, 25 of these are direct dependencies
final var metaComponent = new RepositoryMetaComponent();
metaComponent.setRepositoryType(RepositoryType.MAVEN);
metaComponent.setNamespace("component-group");
metaComponent.setName("component-name-"+i);
metaComponent.setLatestVersion(String.valueOf(i)+".0");
metaComponent.setLastCheck(new Date());
qm.persist(metaComponent);
} else {
// 500 components with no RepositoryMetaComponent containing version
// metadata, all transitive dependencies
}
}
project.setDirectDependencies("[" + String.join(",", directDepencencies.toArray(new String[0])) + "]");
return project;
}

@Test
public void getOutdatedComponentsTest() throws MalformedPackageURLException {
final Project project = prepareProject();

final Response response = target(V1_COMPONENT + "/project/" + project.getUuid())
.queryParam("onlyOutdated", true)
.queryParam("onlyDirect", false)
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("200"); // 200 outdated dependencies, direct and transitive

final JsonArray json = parseJsonArray(response);
assertThat(json).hasSize(100); // Default page size is 100
}

@Test
public void getOutdatedDirectComponentsTest() throws MalformedPackageURLException {
final Project project = prepareProject();

final Response response = target(V1_COMPONENT + "/project/" + project.getUuid())
.queryParam("onlyOutdated", true)
.queryParam("onlyDirect", true)
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("75"); // 75 outdated direct dependencies

final JsonArray json = parseJsonArray(response);
assertThat(json).hasSize(75);
}

@Test
public void getAllComponentsTest() throws MalformedPackageURLException {
final Project project = prepareProject();

final Response response = target(V1_COMPONENT + "/project/" + project.getUuid())
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1000");
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1000"); // 1000 dependencies

final JsonArray json = parseJsonArray(response);
assertThat(json).hasSize(100); // Default page size is 100
}

@Test
public void getAllDirectComponentsTest() throws MalformedPackageURLException {
final Project project = prepareProject();

final Response response = target(V1_COMPONENT + "/project/" + project.getUuid())
.queryParam("onlyDirect", true)
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("100"); // 100 direct dependencies

final JsonArray json = parseJsonArray(response);
assertThat(json).hasSize(100);
}

@Test
public void getComponentByUuidTest() {
Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);
Expand Down

0 comments on commit a7e5122

Please sign in to comment.