From d8e3af4df973733cc6f42c9bb12f139ab76949c8 Mon Sep 17 00:00:00 2001 From: Dustin Decker Date: Fri, 7 Apr 2023 10:15:42 -0700 Subject: [PATCH 001/109] Include Cloud SQL database connectors Signed-off-by: Dustin Decker --- .../_docs/getting-started/database-support.md | 24 +++++++++++++++++++ pom.xml | 15 ++++++++++++ 2 files changed, 39 insertions(+) diff --git a/docs/_docs/getting-started/database-support.md b/docs/_docs/getting-started/database-support.md index e1dcca58e8..46717ba40e 100644 --- a/docs/_docs/getting-started/database-support.md +++ b/docs/_docs/getting-started/database-support.md @@ -75,6 +75,30 @@ alpine.database.url=jdbc:mysql://localhost:3306/dtrack?autoReconnect=true&useSSL MySQL may erroneously report index key length violations ("Specified key was too long"), when in fact the multi-byte key length is lower than the actual value. **Do not use MySQL if don't know how to work around errors like this**! +#### Cloud SQL + +Connecting to Cloud SQL with IAM and mTLS is supported using the Cloud SQL database connectors included. + +More information [here](https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory) + +##### CloudSQL PostgreSQL + +``` +jdbc:postgresql:///?cloudSqlInstance=&socketFactory=com.google.cloud.sql.postgres.SocketFactory +``` + +##### CloudSQL Microsoft SQL Server + +``` +jdbc:sqlserver://localhost;databaseName=;socketFactoryClass=com.google.cloud.sql.sqlserver.SocketFactory;socketFactoryConstructorArg= +``` + +##### CloudSQL MySQL + +``` +jdbc:mysql:///?cloudSqlInstance=&socketFactory=com.google.cloud.sql.mysql.SocketFactory +``` + ### Connection Pooling The Dependency-Track API server utilizes **two** database connection pools - one for *transactional* and one for diff --git a/pom.xml b/pom.xml index 7b5609d12b..cdbfa88e59 100644 --- a/pom.xml +++ b/pom.xml @@ -286,6 +286,21 @@ postgresql ${lib.jdbc-driver.postgresql.version} + + com.google.cloud.sql + mysql-socket-factory-connector-j-8 + 1.11.0 + + + com.google.cloud.sql + postgres-socket-factory + 1.11.0 + + + com.google.cloud.sql + cloud-sql-connector-jdbc-sqlserver + 1.11.0 + xerces From c02fe38e7b8e59172959130119a0bd00daa31ad6 Mon Sep 17 00:00:00 2001 From: Walter de Boer Date: Sun, 12 Mar 2023 09:22:42 +0100 Subject: [PATCH 002/109] Added transient List of ProjectVersions and set Metrics in Project to minimize the number of round trips a client needs to make Signed-off-by: Walter de Boer --- .../org/dependencytrack/model/Project.java | 17 ++++-- .../dependencytrack/model/ProjectVersion.java | 56 +++++++++++++++++++ .../persistence/ProjectQueryManager.java | 36 +++++++++++- .../persistence/QueryManager.java | 7 ++- .../resources/v1/ProjectResource.java | 4 +- .../resources/v1/ProjectResourceTest.java | 44 +++++++++++---- 6 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/ProjectVersion.java diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 791afdaefc..68e9a84845 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -32,7 +32,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; - import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -45,8 +44,8 @@ import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; -import javax.jdo.annotations.Unique; import javax.jdo.annotations.Serialized; +import javax.jdo.annotations.Unique; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -260,6 +259,8 @@ public enum FetchGroup { private transient ProjectMetrics metrics; + private transient List versions; + @JsonIgnore private transient List dependencyGraph; @@ -460,6 +461,14 @@ public void setMetrics(ProjectMetrics metrics) { this.metrics = metrics; } + public List getVersions() { + return versions; + } + + public void setVersions(List versions) { + this.versions = versions; + } + public List getAccessTeams() { return accessTeams; } @@ -499,13 +508,13 @@ public String toString() { return sb.toString(); } } - + private final static class BooleanDefaultTrueSerializer extends JsonSerializer { @Override public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeBoolean(value != null ? value : true); } - + } } diff --git a/src/main/java/org/dependencytrack/model/ProjectVersion.java b/src/main/java/org/dependencytrack/model/ProjectVersion.java new file mode 100644 index 0000000000..23238997ab --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ProjectVersion.java @@ -0,0 +1,56 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.io.Serializable; + +/** + * Value object holding UUID and version for a project + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProjectVersion implements Serializable { + + private static final long serialVersionUID = 1L; + + private String uuid; + + private String version; + + public ProjectVersion(String uuid, String version) { + this.uuid = uuid; + this.version = version; + + } + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getUuid() { + return uuid; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 64486f20a1..96440df9a8 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -31,6 +31,7 @@ import com.github.packageurl.PackageURL; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.Analysis; @@ -41,6 +42,7 @@ import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.ProjectVersion; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; @@ -48,7 +50,6 @@ import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.util.NotificationUtil; - import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import javax.jdo.Query; @@ -193,6 +194,23 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact return execute(query, params); } + /** + * Returns a project by its uuid. + * @param uuid the uuid of the Project (required) + * @return a Project object, or null if not found + */ + public Project getProject(final String uuid) { + final Project project = getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); + if (project != null) { + // set Metrics to minimize the number of round trips a client needs to make + project.setMetrics(getMostRecentProjectMetrics(project)); + // set ProjectVersions to minimize the number of round trips a client needs to make + project.setVersions(getProjectVersions(project)); + } + return project; + } + + /** * Returns a project by its name and version. * @param name the name of the Project (required) @@ -212,7 +230,14 @@ public Project getProject(final String name, final String version) { preprocessACLs(query, queryFilter, params, false); query.setFilter(queryFilter); query.setRange(0, 1); - return singleResult(query.executeWithMap(params)); + final Project project = singleResult(query.executeWithMap(params)); + if (project != null) { + // set Metrics to prevent extra round trip + project.setMetrics(getMostRecentProjectMetrics(project)); + // set ProjectVersions to prevent extra round trip + project.setVersions(getProjectVersions(project)); + } + return project; } /** @@ -1095,4 +1120,11 @@ private static boolean hasActiveChild(Project project) { } return hasActiveChild; } + + private List getProjectVersions(Project project) { + final Query query = pm.newQuery(JDOQuery.SQL, "SELECT UUID, VERSION FROM PROJECT WHERE NAME = ?"); + query.setParameters(project.getName()); + final var stream = query.executeList().stream(); + return stream.map(i -> new ProjectVersion(i[0].toString(), i[1].toString())).toList(); + } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index da42e71319..1f1582e348 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -76,7 +76,6 @@ 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; @@ -359,6 +358,10 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact return getProjectQueryManager().getProjects(name, excludeInactive, onlyRoot); } + public Project getProject(final String uuid) { + return getProjectQueryManager().getProject(uuid); + } + public Project getProject(final String name, final String version) { return getProjectQueryManager().getProject(name, version); } @@ -1361,7 +1364,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()); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 58a8ba3a98..f7c85b3f06 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -39,14 +39,12 @@ import org.dependencytrack.model.Tag; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; - import java.security.Principal; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; - import javax.jdo.FetchGroup; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -115,7 +113,7 @@ public Response getProject( @ApiParam(value = "The UUID of the project to retrieve", required = true) @PathParam("uuid") String uuid) { try (QueryManager qm = new QueryManager()) { - final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); + final Project project = qm.getProject(uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 81a4e92550..89a0334250 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -18,8 +18,8 @@ */ package org.dependencytrack.resources.v1; +import static org.assertj.core.api.Assertions.assertThat; import alpine.common.util.UuidUtil; -import alpine.notification.Notification; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.cyclonedx.model.ExternalReference.Type; @@ -35,7 +35,11 @@ import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Test; - +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; @@ -43,14 +47,6 @@ 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.List; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; public class ProjectResourceTest extends ResourceTest { @@ -63,8 +59,6 @@ protected DeploymentContext configureDeployment() { .build(); } - private static final ConcurrentLinkedQueue NOTIFICATIONS = new ConcurrentLinkedQueue<>(); - @Test public void getProjectsDefaultRequestTest() { for (int i=0; i<1000; i++) { @@ -172,6 +166,29 @@ public void getProjectsByNameActiveOnlyRequestTest() { Assert.assertEquals(100, json.size()); } + @Test + public void getProjectLookupTest() { + for (int i=0; i<500; i++) { + qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, false, false); + } + Response response = target(V1_PROJECT+"/lookup") + .queryParam("name", "Acme Example") + .queryParam("version", "10") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Acme Example", json.getString("name")); + Assert.assertEquals("10", json.getString("version")); + Assert.assertEquals(500, json.getJsonArray("versions").size()); + Assert.assertNotNull(json.getJsonArray("versions").getJsonObject(100).getString("uuid")); + Assert.assertNotEquals("", json.getJsonArray("versions").getJsonObject(100).getString("uuid")); + Assert.assertEquals("100", json.getJsonArray("versions").getJsonObject(100).getString("version")); + } + @Test public void getProjectsAscOrderedRequestTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); @@ -218,6 +235,9 @@ public void getProjectByUuidTest() { JsonObject json = parseJsonObject(response); Assert.assertNotNull(json); Assert.assertEquals("ABC", json.getString("name")); + Assert.assertEquals(1, json.getJsonArray("versions").size()); + Assert.assertEquals(project.getUuid().toString(), json.getJsonArray("versions").getJsonObject(0).getJsonString("uuid").getString()); + Assert.assertEquals("1.0", json.getJsonArray("versions").getJsonObject(0).getJsonString("version").getString()); } @Test From 995c27ed40fb6fac827029867e2b9b56b18968de Mon Sep 17 00:00:00 2001 From: Walter de Boer Date: Sun, 12 Mar 2023 00:27:47 +0100 Subject: [PATCH 003/109] Add onlyOutdated switch to only show outdated components that are direct dependencies of the project Signed-off-by: Walter de Boer --- .../persistence/ComponentQueryManager.java | 39 +++++++++--- .../persistence/QueryManager.java | 7 ++- .../resources/v1/ComponentResource.java | 16 ++--- .../resources/v1/ComponentResourceTest.java | 61 ++++++++++++++++++- 4 files changed, 105 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 7d993de5a4..40a8a8aa6f 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -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 { @@ -133,17 +132,43 @@ public List 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 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 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 { diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index da42e71319..312d14a4f8 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -76,7 +76,6 @@ 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; @@ -881,6 +880,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); } @@ -1361,7 +1364,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()); diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 5050699767..9bf1abe4df 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -40,14 +40,10 @@ 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.model.RepositoryType; 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; @@ -60,6 +56,8 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; /** * JAX-RS resources for processing components. @@ -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(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java index 281a1e902c..038523b629 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java @@ -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; @@ -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 { @@ -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 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); From 575d582d617ffc9102645bfb26e7bc5bb43e2506 Mon Sep 17 00:00:00 2001 From: mhh Date: Wed, 17 May 2023 13:28:07 +0200 Subject: [PATCH 004/109] Make DependencyTrack find custom licenses when uploading CycloneDx files Signed-off-by: mhh --- .../parser/cyclonedx/util/ModelConverter.java | 7 +++++++ .../persistence/LicenseQueryManager.java | 12 ++++++++++++ .../dependencytrack/persistence/QueryManager.java | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index cde709bdd1..5cd70e7d33 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -163,6 +163,13 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model component.setResolvedLicense(license); } } + else if (StringUtils.isNotBlank(cycloneLicense.getName())) + { + final License license = qm.getCustomLicense(StringUtils.trimToNull(cycloneLicense.getName())); + if (license != null) { + component.setResolvedLicense(license); + } + } component.setLicense(StringUtils.trimToNull(cycloneLicense.getName())); component.setLicenseUrl(StringUtils.trimToNull(cycloneLicense.getUrl())); } diff --git a/src/main/java/org/dependencytrack/persistence/LicenseQueryManager.java b/src/main/java/org/dependencytrack/persistence/LicenseQueryManager.java index f62849432a..bb04c96aef 100644 --- a/src/main/java/org/dependencytrack/persistence/LicenseQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/LicenseQueryManager.java @@ -93,6 +93,18 @@ public License getLicense(String licenseId) { return singleResult(query.execute(licenseId)); } + /** + * Returns a Custom License object from the specified name + * @param licenseName license name of custom license + * @return a License object, or null if not found + */ + public License getCustomLicense(String licenseName) { + final Query query = pm.newQuery(License.class, "name == :name && customLicense == true"); + query.getFetchPlan().addGroup(License.FetchGroup.ALL.name()); + query.setRange(0, 1); + return singleResult(query.execute(licenseName)); + } + /** * Creates a new License. * @param license the License object to create diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index da42e71319..299d10c683 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -548,6 +548,11 @@ public License getLicense(String licenseId) { return getLicenseQueryManager().getLicense(licenseId); } + public License getCustomLicense(String licenseName) + { + return getLicenseQueryManager().getCustomLicense(licenseName); + } + License synchronizeLicense(License license, boolean commitIndex) { return getLicenseQueryManager().synchronizeLicense(license, commitIndex); } From 699c52e4496d27065b82696267c320af4d3fd176 Mon Sep 17 00:00:00 2001 From: mhh Date: Wed, 17 May 2023 13:46:09 +0200 Subject: [PATCH 005/109] Move { Signed-off-by: mhh --- .../java/org/dependencytrack/persistence/QueryManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 299d10c683..299e829c1b 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -548,8 +548,7 @@ public License getLicense(String licenseId) { return getLicenseQueryManager().getLicense(licenseId); } - public License getCustomLicense(String licenseName) - { + public License getCustomLicense(String licenseName) { return getLicenseQueryManager().getCustomLicense(licenseName); } From f1ce89668a848a85a0fc8aaea575b8d1cfb35119 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 12 Jun 2023 19:10:33 +0200 Subject: [PATCH 006/109] Fix GHA `set-output` deprecation warnings See https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ Signed-off-by: nscuro --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-publish.yaml | 2 +- .github/workflows/ci-release.yaml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 23155789a7..042037ae69 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -102,7 +102,7 @@ jobs: if [[ "${{ inputs.app-version }}" != "snapshot" ]]; then TAGS="${TAGS},docker.io/dependencytrack/${{ matrix.distribution }}:latest" fi - echo "::set-output name=tags::${TAGS}" + echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build multi-arch Container Image uses: docker/build-push-action@v4.1.0 diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index f579ee70f4..1801348082 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -27,7 +27,7 @@ jobs: id: parse run: |- VERSION=`yq -p=xml '.project.version' pom.xml` - echo "::set-output name=version::${VERSION}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT call-build: needs: diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index 4172d627ec..03f5c919cd 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -30,9 +30,9 @@ jobs: fi NEXT_VERSION="${VERSION%.*}.$((${VERSION##*.} + 1))-SNAPSHOT" - echo "::set-output name=version::${VERSION}" - echo "::set-output name=next-version::${NEXT_VERSION}" - echo "::set-output name=release-branch::${VERSION%.*}.x" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "next-version=${NEXT_VERSION}" >> $GITHUB_OUTPUT + echo "release-branch=${VERSION%.*}.x" >> $GITHUB_OUTPUT create-release: runs-on: ubuntu-latest From b74a2d31d603f28fe183011a70532e46e6a55592 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 12 Jun 2023 20:20:39 +0200 Subject: [PATCH 007/109] Bump Alpine to `2.2.3-SNAPSHOT` Includes https://github.com/stevespringett/Alpine/pull/494 among other changes so far. Also added documentation for `alpine.datanucleus.cache.level2.type`. Signed-off-by: nscuro --- docs/_docs/getting-started/configuration.md | 13 +++++++++++++ pom.xml | 2 +- src/main/resources/application.properties | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index acbef529ce..2aa8413ede 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -149,6 +149,19 @@ alpine.database.pool.max.lifetime=600000 # alpine.database.pool.tx.max.lifetime= # alpine.database.pool.nontx.max.lifetime= +# Optional +# Controls the 2nd level cache type used by DataNucleus, the Object Relational Mapper (ORM). +# See https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#cache_level2 +# Values supported by Dependency-Track are "soft" (default), "weak", and "none". +# +# Setting this property to "none" may help in reducing the memory footprint of Dependency-Track, +# but has the potential to slow down database operations. +# Size of the cache may be monitored through the "datanucleus_cache_second_level_entries" metric, +# refer to https://docs.dependencytrack.org/getting-started/monitoring/#metrics for details. +# +# DO NOT CHANGE UNLESS THERE IS A GOOD REASON TO. +# alpine.datanucleus.cache.level2.type= + # Optional # When authentication is enforced, API keys are required for automation, and # the user interface will prevent anonymous access by prompting for login diff --git a/pom.xml b/pom.xml index 351b7ffb53..176726e5db 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 2.2.2 + 2.2.3-SNAPSHOT 4.0.0 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0f4b1df289..2e22e3f533 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -115,6 +115,19 @@ alpine.database.pool.max.lifetime=600000 # alpine.database.pool.tx.max.lifetime= # alpine.database.pool.nontx.max.lifetime= +# Optional +# Controls the 2nd level cache type used by DataNucleus, the Object Relational Mapper (ORM). +# See https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#cache_level2 +# Values supported by Dependency-Track are "soft" (default), "weak", and "none". +# +# Setting this property to "none" may help in reducing the memory footprint of Dependency-Track, +# but has the potential to slow down database operations. +# Size of the cache may be monitored through the "datanucleus_cache_second_level_entries" metric, +# refer to https://docs.dependencytrack.org/getting-started/monitoring/#metrics for details. +# +# DO NOT CHANGE UNLESS THERE IS A GOOD REASON TO. +# alpine.datanucleus.cache.level2.type= + # Optional # When authentication is enforced, API keys are required for automation, # and the user interface will prevent anonymous access by prompting for login From c5e7a1373885601d954093863906f3186fabf006 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:56:49 +0000 Subject: [PATCH 008/109] Bump json from 20230227 to 20230618 Bumps [json](https://github.com/douglascrockford/JSON-java) from 20230227 to 20230618. - [Release notes](https://github.com/douglascrockford/JSON-java/releases) - [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md) - [Commits](https://github.com/douglascrockford/JSON-java/commits) --- updated-dependencies: - dependency-name: org.json:json dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 176726e5db..11c1a3527b 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.json json - 20230227 + 20230618 From d8fa43a3b4c689cbd318fde179b4a63dd44d576b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:57:55 +0000 Subject: [PATCH 009/109] Bump debian in /src/main/docker Bumps debian from bullseye-20230522-slim to bullseye-20230612-slim. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 50b357f4f2..e3289093d4 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,6 +1,6 @@ FROM eclipse-temurin:17.0.6_10-jre-focal@sha256:22942ca3ffac6e593063e33a225c458e315afa2c0dddfdbe15d337dd9130c70c AS jre-build -FROM debian:bullseye-20230522-slim@sha256:7606bef5684b393434f06a50a3d1a09808fee5a0240d37da5d181b1b121e7637 +FROM debian:bullseye-20230612-slim@sha256:924df86f8aad741a0134b2de7d8e70c5c6863f839caadef62609c1be1340daf5 # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From 7f4206d74eb00b7c35f22c80a07e6e8bfc9f919a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:58:28 +0000 Subject: [PATCH 010/109] Bump docker/setup-buildx-action from 2.6.0 to 2.7.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.6.0 to 2.7.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2.6.0...v2.7.0) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 042037ae69..94316d3216 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -82,7 +82,7 @@ jobs: uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.6.0 + uses: docker/setup-buildx-action@v2.7.0 id: buildx with: install: true From 871e811c4dadc0611971cf7f6a3cd20a910a841a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:58:32 +0000 Subject: [PATCH 011/109] Bump docker/build-push-action from 4.1.0 to 4.1.1 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 042037ae69..b37a8bdaad 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -105,7 +105,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build multi-arch Container Image - uses: docker/build-push-action@v4.1.0 + uses: docker/build-push-action@v4.1.1 with: tags: ${{ steps.tags.outputs.tags }} build-args: |- From e8b24a462e9cdbbefa673c539b1562449a78dd5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:58:36 +0000 Subject: [PATCH 012/109] Bump docker/setup-qemu-action from 2.1.0 to 2.2.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v2.1.0...v2.2.0) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 042037ae69..c2e6407592 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -79,7 +79,7 @@ jobs: path: target - name: Set up QEMU - uses: docker/setup-qemu-action@v2.1.0 + uses: docker/setup-qemu-action@v2.2.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2.6.0 From ac260b233b437bc66ec38322640ed9ac0f70e70c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 08:56:53 +0000 Subject: [PATCH 013/109] Bump maven-clean-plugin from 3.2.0 to 3.3.1 Bumps [maven-clean-plugin](https://github.com/apache/maven-clean-plugin) from 3.2.0 to 3.3.1. - [Release notes](https://github.com/apache/maven-clean-plugin/releases) - [Commits](https://github.com/apache/maven-clean-plugin/compare/maven-clean-plugin-3.2.0...maven-clean-plugin-3.3.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-clean-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 176726e5db..3b11f06e19 100644 --- a/pom.xml +++ b/pom.xml @@ -510,7 +510,7 @@ org.apache.maven.plugins maven-clean-plugin - 3.2.0 + 3.3.1 true From b5330ebff1f78dc69210d4bd1d2fab414e94454b Mon Sep 17 00:00:00 2001 From: Walter de Boer Date: Fri, 23 Jun 2023 23:03:01 +0200 Subject: [PATCH 014/109] new onlyDirect switch do optionally exclude transitive dependencies Signed-off-by: Walter de Boer --- .../persistence/ComponentQueryManager.java | 16 ++- .../persistence/QueryManager.java | 4 +- .../resources/v1/ComponentResource.java | 8 +- .../resources/v1/ComponentResourceTest.java | 98 +++++++++++++------ 4 files changed, 87 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 40a8a8aa6f..0f1978892f 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -136,33 +136,39 @@ public List getAllComponents(Project project) { * @return a List of Dependency objects */ public PaginatedResult getComponents(final Project project, final boolean includeMetrics) { - return getComponents(project, includeMetrics, false); + 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) { + public PaginatedResult getComponents(final Project project, final boolean includeMetrics, final boolean onlyOutdated, final boolean onlyDirect) { final PaginatedResult result; 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 += - " && 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 " + + " && 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 query = pm.newQuery(querySring); query.getFetchPlan().setMaxFetchDepth(2); if (orderBy == null) { diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 312d14a4f8..4ed133e5b6 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -880,8 +880,8 @@ 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 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) { diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 9bf1abe4df..8a3b4b1341 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -87,13 +87,15 @@ public class ComponentResource extends AlpineResource { 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) { + @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, onlyOutdated); + 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(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java index 038523b629..edd7069ce0 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java @@ -65,10 +65,17 @@ public void getComponentsDefaultRequestTest() { Assert.assertEquals(405, response.getStatus()); // No longer prohibited in DT 4.0+ } - @Test - public void getOutdatedComponentsTest() throws MalformedPackageURLException { + /** + * Generate a project with different dependencies + * @return A project with 1000 dpendencies: