Skip to content

Commit

Permalink
Add onlyOutdated switch to only show outdated components that are dir…
Browse files Browse the repository at this point in the history
…ect dependencies of the project

Signed-off-by: Walter de Boer <[email protected]>
  • Loading branch information
Walter de Boer committed Mar 6, 2023
1 parent 25e1c67 commit 8100665
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,35 @@
*/
package org.dependencytrack.persistence;

import alpine.common.logging.Logger;
import alpine.event.framework.Event;
import alpine.model.ApiKey;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import org.dependencytrack.event.IndexEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.ConfigPropertyConstants;
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 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.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.json.Json;
import javax.json.JsonValue;
import javax.json.JsonArray;
import javax.json.JsonValue;
import org.dependencytrack.event.IndexEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import alpine.common.logging.Logger;
import alpine.event.framework.Event;
import alpine.model.ApiKey;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;

final class ComponentQueryManager extends QueryManager implements IQueryManager {

Expand Down Expand Up @@ -133,17 +132,43 @@ 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);
}
/**
* 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
* @return a List of Dependency objects
*/
public PaginatedResult getComponents(final Project project, final boolean includeMetrics, final boolean onlyOutdated) {
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) {
// Hack JDO using % instead of .* to get the SQL LIKE clause working:
querySring +=
" && this.project.directDependencies.matches('%\"uuid\":\"'+this.uuid+'\"%') " + // only direct dependencies
" && ("+
" 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()";
}
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
51 changes: 27 additions & 24 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
*/
package org.dependencytrack.persistence;

import alpine.common.util.BooleanUtil;
import alpine.event.framework.Event;
import alpine.model.ApiKey;
import alpine.model.ConfigProperty;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.notification.NotificationLevel;
import alpine.persistence.AlpineQueryManager;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.github.packageurl.PackageURL;
import java.security.Principal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.json.JsonObject;
import org.datanucleus.PropertyNames;
import org.datanucleus.api.jdo.JDOQuery;
import org.dependencytrack.event.IndexEvent;
Expand Down Expand Up @@ -76,18 +76,17 @@
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.Publisher;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;

import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.json.JsonObject;
import java.security.Principal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.github.packageurl.PackageURL;
import alpine.common.util.BooleanUtil;
import alpine.event.framework.Event;
import alpine.model.ApiKey;
import alpine.model.ConfigProperty;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.notification.NotificationLevel;
import alpine.persistence.AlpineQueryManager;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;

/**
* This QueryManager provides a concrete extension of {@link AlpineQueryManager} by
Expand Down Expand Up @@ -880,6 +879,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) {
return getComponentQueryManager().getComponents(project, includeMetrics, onlyOutdated);
}

public ServiceComponent matchServiceIdentity(final Project project, final ComponentIdentity cid) {
return getServiceComponentQueryManager().matchServiceIdentity(project, cid);
}
Expand Down Expand Up @@ -1321,7 +1324,7 @@ public void recursivelyDeleteTeam(Team team) {
pm.currentTransaction().begin();
pm.deletePersistentAll(team.getApiKeys());
String aclDeleteQuery = """
DELETE FROM \"PROJECT_ACCESS_TEAMS\" WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ?
DELETE FROM \"PROJECT_ACCESS_TEAMS\" WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ?
""";
final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, aclDeleteQuery);
query.executeWithArray(team.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,8 @@
*/
package org.dependencytrack.resources.v1;

import alpine.event.framework.Event;
import alpine.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import io.swagger.annotations.ResponseHeader;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.InternalComponentIdentificationEvent;
import org.dependencytrack.event.RepositoryMetaEvent;
import org.dependencytrack.event.VulnerabilityAnalysisEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.License;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.InternalComponentIdentificationUtil;

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

import javax.validation.Validator;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
Expand All @@ -60,6 +32,32 @@
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.InternalComponentIdentificationEvent;
import org.dependencytrack.event.RepositoryMetaEvent;
import org.dependencytrack.event.VulnerabilityAnalysisEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.License;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.InternalComponentIdentificationUtil;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import alpine.event.framework.Event;
import alpine.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import io.swagger.annotations.ResponseHeader;

/**
* JAX-RS resources for processing components.
Expand All @@ -86,12 +84,16 @@ 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 and indirect dependencies so only outdated are returned", required = false)
@QueryParam("onlyOutdated") boolean onlyOutdated) {
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);
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 @@ -18,9 +18,16 @@
*/
package org.dependencytrack.resources.v1;

import alpine.common.util.UuidUtil;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
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 org.apache.http.HttpStatus;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.Component;
Expand All @@ -33,16 +40,11 @@
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.Date;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import alpine.common.util.UuidUtil;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;

public class ComponentResourceTest extends ResourceTest {

Expand All @@ -63,6 +65,61 @@ public void getComponentsDefaultRequestTest() {
Assert.assertEquals(405, response.getStatus()); // No longer prohibited in DT 4.0+
}

@Test
public void getOutdatedComponentsTest() throws MalformedPackageURLException {
final Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);
final List<String> directDepencencies = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Component component = new Component();
component.setProject(project);
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);
if (i<100) {
if ((i >= 25) && (i < 75)) {
// 50 recent direct depencencies
directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}");
}
// same version
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);
}
if (i>=100 && i<200) {
if ((i >= 150) && (i < 175)) {
// 25 outdated direct depencencies
directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}");
}
// newer version
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);
}
}
project.setDirectDependencies("[" + String.join(",", directDepencencies.toArray(new String[0])) + "]");

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

final JsonArray json = parseJsonArray(response);
assertThat(json).hasSize(25); // Only 25 direct dependencies
}

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

0 comments on commit 8100665

Please sign in to comment.