diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 13792160b..a44a58ed7 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -24,6 +24,9 @@ import alpine.resources.AlpineRequest; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonValue; import org.apache.commons.lang3.tuple.Pair; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; @@ -35,12 +38,8 @@ import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; import org.dependencytrack.tasks.IntegrityMetaInitializerTask; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonValue; import javax.jdo.PersistenceManager; import javax.jdo.Query; -import javax.jdo.Transaction; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; @@ -609,55 +608,6 @@ protected void deleteComponents(Project project) { } } - /** - * Deletes a Component and all objects dependant on the component. - * - * @param component the Component to delete - * @param commitIndex specifies if the search index should be committed (an expensive operation) - */ - public void recursivelyDelete(Component component, boolean commitIndex) { - final Transaction trx = pm.currentTransaction(); - final boolean isJoiningExistingTrx = trx.isActive(); - try { - if (!isJoiningExistingTrx) { - trx.begin(); - } - - for (final Component child : component.getChildren()) { - recursivelyDelete(child, false); - pm.flush(); - } - - // Use bulk DELETE queries to avoid having to fetch every single object from the database first. - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE component == :component"), component); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component == :component"), component); - - - // The component itself must be deleted via deletePersistentAll, otherwise relationships - // (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly. - final Query componentQuery = pm.newQuery(Component.class, "this == :component"); - try { - componentQuery.deletePersistentAll(component); - } finally { - componentQuery.closeAll(); - } - - if (!isJoiningExistingTrx) { - trx.commit(); - } - } finally { - if (!isJoiningExistingTrx && trx.isActive()) { - trx.rollback(); - } - } - } - /** * Returns a list of components by matching its identity information. * diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 6bd764c67..94dd98f15 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -818,54 +818,6 @@ private static Set parseDirectDependenciesUuids( return uuids; } - /** - * Deletes a Project and all objects dependent on the project. - * - * @param project the Project to delete - * @param commitIndex specifies if the search index should be committed (an expensive operation) - */ - @Override - public void recursivelyDelete(final Project project, final boolean commitIndex) { - runInTransaction(() -> { - for (final Project child : project.getChildren()) { - // Note: This could be refactored such that each project is deleted - // in its own transaction. That would break semantics when it comes - // to joining an existing transaction though, so needs a bit more thought. - recursivelyDelete(child, false); - pm.flush(); - } - - // Use bulk DELETE queries to avoid having to fetch every single object from the database first. - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetrics WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component.project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Bom WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Vex WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetadata WHERE project == :project"), project); - executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectProperty WHERE project == :project"), project); - - // Projects, Components, and ServiceComponents must be deleted via deletePersistentAll, otherwise relationships - // (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly. - deleteComponents(project); - deleteServiceComponents(project); - removeProjectFromNotificationRules(project); - removeProjectFromPolicies(project); - - final Query projectQuery = pm.newQuery(Project.class, "this == :project"); - try { - projectQuery.deletePersistentAll(project); - } finally { - projectQuery.closeAll(); - } - }); - } - /** * Creates a key/value pair (ProjectProperty) for the specified Project. * diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index ed07d19d4..f0c7b8bcf 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -32,7 +32,6 @@ import alpine.persistence.NotSortableException; import alpine.persistence.OrderDirection; import alpine.persistence.PaginatedResult; -import alpine.persistence.ScopedCustomization; import alpine.resources.AlpineRequest; import alpine.server.util.DbUtil; import com.github.packageurl.PackageURL; @@ -128,7 +127,6 @@ import java.util.concurrent.Callable; import java.util.function.Predicate; -import static org.datanucleus.PropertyNames.PROPERTY_QUERY_SQL_ALLOWALL; import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -653,10 +651,6 @@ public Project updateLastBomImport(Project p, Date date, String bomFormat) { return getProjectQueryManager().updateLastBomImport(p, date, bomFormat); } - public void recursivelyDelete(final Project project, final boolean commitIndex) { - getProjectQueryManager().recursivelyDelete(project, commitIndex); - } - public ProjectProperty createProjectProperty(final Project project, final String groupName, final String propertyName, final String propertyValue, final ProjectProperty.PropertyType propertyType, final String description) { @@ -743,10 +737,6 @@ void deleteComponents(Project project) { getComponentQueryManager().deleteComponents(project); } - public void recursivelyDelete(Component component, boolean commitIndex) { - getComponentQueryManager().recursivelyDelete(component, commitIndex); - } - public Map getDependencyGraphForComponents(Project project, List components) { return getComponentQueryManager().getDependencyGraphForComponents(project, components); } @@ -1015,10 +1005,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability, getVulnerabilityQueryManager().deleteAffectedVersionAttribution(vulnerability, vulnerableSoftware, source); } - public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) { - getVulnerabilityQueryManager().deleteAffectedVersionAttributions(vulnerability); - } - public boolean contains(Vulnerability vulnerability, Component component) { return getVulnerabilityQueryManager().contains(vulnerability, component); } @@ -1119,14 +1105,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService return getServiceComponentQueryManager().updateServiceComponent(transientServiceComponent, commitIndex); } - public void deleteServiceComponents(final Project project) { - getServiceComponentQueryManager().deleteServiceComponents(project); - } - - public void recursivelyDelete(ServiceComponent service, boolean commitIndex) { - getServiceComponentQueryManager().recursivelyDelete(service, commitIndex); - } - public PaginatedResult getVulnerabilities() { return getVulnerabilityQueryManager().getVulnerabilities(); } @@ -1618,20 +1596,6 @@ public T runInRetryableTransaction(final Callable supplier, final Predica .executeSupplier(() -> callInTransaction(supplier)); } - public void recursivelyDeleteTeam(Team team) { - runInTransaction(() -> { - pm.deletePersistentAll(team.getApiKeys()); - - try (var ignored = new ScopedCustomization(pm).withProperty(PROPERTY_QUERY_SQL_ALLOWALL, "true")) { - final Query aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """ - DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?"""); - executeAndCloseWithArray(aclDeleteQuery, team.getId()); - } - - pm.deletePersistent(team); - }); - } - /** * Returns a list of all {@link DependencyGraphResponse} objects by {@link Component} UUID. * diff --git a/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java index 00771b54a..0c55dbc57 100644 --- a/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java @@ -27,7 +27,6 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; -import javax.jdo.Transaction; import java.util.List; import java.util.UUID; @@ -194,63 +193,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService return result; } - /** - * Deletes all services for the specified Project. - * @param project the Project to delete services of - */ - public void deleteServiceComponents(Project project) { - final Query query = pm.newQuery(ServiceComponent.class, "project == :project"); - try { - query.deletePersistentAll(project); - } finally { - query.closeAll(); - } - } - - /** - * Deletes a ServiceComponent and all objects dependant on the service. - * @param service the ServiceComponent to delete - * @param commitIndex specifies if the search index should be committed (an expensive operation) - */ - public void recursivelyDelete(ServiceComponent service, boolean commitIndex) { - final Transaction trx = pm.currentTransaction(); - final boolean isJoiningExistingTrx = trx.isActive(); - try { - if (!isJoiningExistingTrx) { - trx.begin(); - } - - for (final ServiceComponent child : service.getChildren()) { - recursivelyDelete(child, false); - pm.flush(); - } - - // TODO: Add these in when these features are supported by service components - //deleteAnalysisTrail(service); - //deleteViolationAnalysisTrail(service); - //deleteMetrics(service); - //deleteFindingAttributions(service); - //deletePolicyViolations(service); - - final Query query = pm.newQuery(ServiceComponent.class); - query.setFilter("this == :service"); - try { - query.deletePersistentAll(service); - } finally { - query.closeAll(); - } - - if (!isJoiningExistingTrx) { - trx.commit(); - } - - } finally { - if (!isJoiningExistingTrx && trx.isActive()) { - trx.rollback(); - } - } - } - /** * Returns a list of all {@link DependencyGraphResponse} objects by {@link ServiceComponent} UUID. * @param uuids a list of {@link ServiceComponent} UUIDs diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 313721cde..b1e35a3ac 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -1013,20 +1013,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability, query.deletePersistentAll(); } - /** - * Delete all {@link AffectedVersionAttribution}s associated with a given {@link Vulnerability}. - * - * @param vulnerability The {@link Vulnerability} to delete {@link AffectedVersionAttribution}s for - * @since 4.7.0 - */ - @Override - public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) { - final Query query = pm.newQuery(AffectedVersionAttribution.class); - query.setFilter("vulnerability == :vulnerability"); - query.setParameters(vulnerability); - query.deletePersistentAll(); - } - /** * Binds the two objects together in a corresponding join table. * diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java new file mode 100644 index 000000000..4577fdcc0 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java @@ -0,0 +1,34 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.jdbi; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +import java.util.UUID; + +public interface ComponentDao { + + @SqlUpdate(""" + DELETE + FROM "COMPONENT" + WHERE "UUID" = :componentUuid + """) + int deleteComponent(@Bind final UUID componentUuid); +} diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java b/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java index 9eb0b544f..c32c3a543 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java @@ -153,7 +153,7 @@ private static Jdbi createJdbi(final PersistenceManager pm) { * @throws IllegalStateException When the given {@link QueryManager} is not participating * in an active {@link javax.jdo.Transaction} */ - static Jdbi createLocalJdbi(final QueryManager qm) { + public static Jdbi createLocalJdbi(final QueryManager qm) { return createLocalJdbi(qm.getPersistenceManager()); } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java index 136501ec8..000756c89 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java @@ -26,6 +26,7 @@ import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; import java.time.Instant; import java.util.List; @@ -220,4 +221,10 @@ record ConciseProjectMetricsRow( ) { } + @SqlUpdate(""" + DELETE + FROM "PROJECT" + WHERE "UUID" = :projectUuid + """) + int deleteProject(@Bind final UUID projectUuid); } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/ServiceComponentDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/ServiceComponentDao.java new file mode 100644 index 000000000..8f964f216 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/ServiceComponentDao.java @@ -0,0 +1,34 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.jdbi; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +import java.util.UUID; + +public interface ServiceComponentDao { + + @SqlUpdate(""" + DELETE + FROM "SERVICECOMPONENT" + WHERE "UUID" = :serviceComponentUuid + """) + int deleteServiceComponent(@Bind final UUID serviceComponentUuid); +} diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java new file mode 100644 index 000000000..dced37e42 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java @@ -0,0 +1,32 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.jdbi; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +public interface TeamDao { + + @SqlUpdate(""" + DELETE + FROM "TEAM" + WHERE "ID" = :teamId + """) + int deleteTeam(@Bind final long teamId); +} diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java index 22e192ae1..a8199f729 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java @@ -431,4 +431,10 @@ WHERE NOT EXISTS( """) int deleteOrphanVulnerableSoftware(); + @SqlUpdate(""" + DELETE + FROM "VULNERABILITY" + WHERE "UUID" = :vulnUuid + """) + int deleteVulnerability(@Bind final UUID vulnUuid); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 5b01be671..510e5074b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -68,10 +68,12 @@ import org.dependencytrack.model.VulnerabilityScan; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.ComponentDao; import org.dependencytrack.proto.repometaanalysis.v1.FetchMeta; import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.util.InternalComponentIdentifier; import org.dependencytrack.util.PurlUtil; +import org.jdbi.v3.core.Handle; import java.util.ArrayList; import java.util.List; @@ -81,6 +83,7 @@ import static org.dependencytrack.event.kafka.componentmeta.IntegrityCheck.calculateIntegrityResult; import static org.dependencytrack.model.FetchStatus.NOT_AVAILABLE; import static org.dependencytrack.model.FetchStatus.PROCESSED; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; /** * JAX-RS resources for processing components. @@ -632,7 +635,10 @@ public Response deleteComponent( if (!qm.hasAccess(super.getPrincipal(), component.getProject())) { return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); } - qm.recursivelyDelete(component, false); + try (final Handle jdbiHandle = openJdbiHandle()) { + final var componentDao = jdbiHandle.attach(ComponentDao.class); + componentDao.deleteComponent(component.getUuid()); + } return Response.status(Response.Status.NO_CONTENT).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the component could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index eaf209146..46170fbe9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -69,6 +69,7 @@ import org.dependencytrack.resources.v1.vo.BomUploadResponse; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; import org.dependencytrack.resources.v1.vo.ConciseProject; +import org.jdbi.v3.core.Handle; import javax.jdo.FetchGroup; import java.security.Principal; @@ -84,6 +85,7 @@ import static alpine.event.framework.Event.isEventBeingProcessed; import static java.util.Objects.requireNonNullElseGet; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.createLocalJdbi; import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.util.PersistenceUtil.isPersistent; import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation; @@ -873,8 +875,10 @@ public Response deleteProject( } LOGGER.info("Project " + project + " deletion request by " + super.getPrincipal().getName()); - try { - qm.recursivelyDelete(project, true); + + try (final Handle jdbiHandle = createLocalJdbi(qm).open()) { + final var projectDao = jdbiHandle.attach(ProjectDao.class); + projectDao.deleteProject(project.getUuid()); } catch (RuntimeException e) { LOGGER.error("Failed to delete project", e); throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR); diff --git a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java index 9a132b81e..efe0788dd 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java @@ -49,7 +49,11 @@ import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.ServiceComponentDao; import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.jdbi.v3.core.Handle; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; /** * JAX-RS resources for processing services. @@ -276,7 +280,10 @@ public Response deleteService( if (!qm.hasAccess(super.getPrincipal(), service.getProject())) { return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified service is forbidden").build(); } - qm.recursivelyDelete(service, false); + try (final Handle jdbiHandle = openJdbiHandle()) { + final var serviceComponentDao = jdbiHandle.attach(ServiceComponentDao.class); + serviceComponentDao.deleteServiceComponent(service.getUuid()); + } return Response.status(Response.Status.NO_CONTENT).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the service could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index 1961dea98..bde594a17 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -50,8 +50,10 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.TeamDao; import org.dependencytrack.resources.v1.vo.TeamSelfResponse; import org.dependencytrack.resources.v1.vo.VisibleTeams; +import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; import java.security.Principal; @@ -59,6 +61,7 @@ import java.util.List; import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; /** * JAX-RS resources for processing teams. @@ -215,7 +218,10 @@ public Response deleteTeam(Team jsonTeam) { final Team team = qm.getObjectByUuid(Team.class, jsonTeam.getUuid(), Team.FetchGroup.ALL.name()); if (team != null) { String teamName = team.getName(); - qm.recursivelyDeleteTeam(team); + try (final Handle jdbiHandle = openJdbiHandle()) { + final var teamDao = jdbiHandle.attach(TeamDao.class); + teamDao.deleteTeam(team.getId()); + } super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Team deleted: " + teamName); return Response.status(Response.Status.NO_CONTENT).build(); } else { diff --git a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java index 497ca2a40..0d8596e2a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java @@ -61,6 +61,7 @@ import org.dependencytrack.resources.v1.vo.AffectedComponent; import org.dependencytrack.resources.v1.vo.AffectedProject; import org.dependencytrack.util.VulnerabilityUtil; +import org.jdbi.v3.core.Handle; import us.springett.cvss.Cvss; import us.springett.cvss.Score; import us.springett.owasp.riskrating.MissingFactorException; @@ -71,6 +72,7 @@ import java.util.List; import java.util.UUID; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; /** @@ -493,8 +495,10 @@ public Response deleteVulnerability( if (vulnerability.getComponents().size() > 0) { return Response.status(Response.Status.PRECONDITION_FAILED).entity("Portfolio components or services are affected by this vulnerability. Unable to delete.").build(); } else { - qm.deleteAffectedVersionAttributions(vulnerability); - qm.delete(vulnerability); + try (final Handle jdbiHandle = openJdbiHandle()) { + final var vulnerabilityDao = jdbiHandle.attach(VulnerabilityDao.class); + vulnerabilityDao.deleteVulnerability(vulnerability.getUuid()); + } return Response.status(Response.Status.NO_CONTENT).build(); } } else { diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index cafaf2c2a..ef25e3fc6 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -189,4 +189,451 @@ CREATE INDEX "COMPONENT_SHA3_512_IDX" ON "COMPONENT" ("SHA3_512") WHERE "SHA3_512" IS NOT NULL; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/persistence/ComponentQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/ComponentQueryManagerTest.java index fde6198f7..98a1c3fbc 100644 --- a/src/test/java/org/dependencytrack/persistence/ComponentQueryManagerTest.java +++ b/src/test/java/org/dependencytrack/persistence/ComponentQueryManagerTest.java @@ -19,127 +19,16 @@ package org.dependencytrack.persistence; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.Analysis; -import org.dependencytrack.model.AnalysisJustification; -import org.dependencytrack.model.AnalysisResponse; -import org.dependencytrack.model.AnalysisState; -import org.dependencytrack.model.AnalyzerIdentity; import org.dependencytrack.model.Component; -import org.dependencytrack.model.DependencyMetrics; -import org.dependencytrack.model.IntegrityAnalysis; -import org.dependencytrack.model.IntegrityMatchStatus; -import org.dependencytrack.model.Policy; -import org.dependencytrack.model.PolicyCondition; -import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; -import org.dependencytrack.model.ViolationAnalysis; -import org.dependencytrack.model.ViolationAnalysisState; -import org.dependencytrack.model.Vulnerability; import org.junit.Test; -import javax.jdo.JDOObjectNotFoundException; -import java.util.Date; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNoException; public class ComponentQueryManagerTest extends PersistenceCapableTest { - @Test - public void recursivelyDeleteTest() { - final var project = new Project(); - project.setName("acme-app"); - project.setVersion("1.0.0"); - qm.persist(project); - - final var component = new Component(); - component.setProject(project); - component.setName("acme-lib"); - component.setVersion("2.0.0"); - qm.persist(component); - - // Assign a vulnerability and an accompanying analysis with comments to component. - final var vuln = new Vulnerability(); - vuln.setVulnId("INT-123"); - vuln.setSource(Vulnerability.Source.INTERNAL); - qm.persist(vuln); - qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); - final Analysis analysis = qm.makeAnalysis(component, vuln, - AnalysisState.NOT_AFFECTED, - AnalysisJustification.CODE_NOT_REACHABLE, - AnalysisResponse.WORKAROUND_AVAILABLE, - "analysisDetails", false); - qm.makeAnalysisComment(analysis, "someComment", "someCommenter"); - - // Create a child component to validate that deletion is indeed recursive. - final var componentChild = new Component(); - componentChild.setProject(project); - componentChild.setParent(component); - componentChild.setName("acme-sub-lib"); - componentChild.setVersion("3.0.0"); - qm.persist(componentChild); - - // Assign a policy violation and an accompanying analysis with comments to componentChild. - final var policy = new Policy(); - policy.setName("Test Policy"); - policy.setViolationState(Policy.ViolationState.WARN); - policy.setOperator(Policy.Operator.ALL); - qm.persist(policy); - final var policyCondition = new PolicyCondition(); - policyCondition.setPolicy(policy); - policyCondition.setSubject(PolicyCondition.Subject.COORDINATES); - policyCondition.setOperator(PolicyCondition.Operator.MATCHES); - policyCondition.setValue("someValue"); - qm.persist(policyCondition); - final var policyViolation = new PolicyViolation(); - policyViolation.setPolicyCondition(policyCondition); - policyViolation.setComponent(componentChild); - policyViolation.setType(PolicyViolation.Type.OPERATIONAL); - policyViolation.setTimestamp(new Date()); - qm.persist(policyViolation); - final ViolationAnalysis violationAnalysis = qm.makeViolationAnalysis(componentChild, policyViolation, - ViolationAnalysisState.REJECTED, false); - qm.makeViolationAnalysisComment(violationAnalysis, "someComment", "someCommenter"); - - // Assign am integrity analysis to componentChild - final var integrityAnalysis = new IntegrityAnalysis(); - integrityAnalysis.setComponent(componentChild); - integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha1HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha256HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha512HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setUpdatedAt(new Date()); - qm.persist(integrityAnalysis); - - // Create metrics for component. - final var metrics = new DependencyMetrics(); - metrics.setProject(project); - metrics.setComponent(component); - metrics.setFirstOccurrence(new Date()); - metrics.setLastOccurrence(new Date()); - qm.persist(metrics); - - assertThatNoException() - .isThrownBy(() -> qm.recursivelyDelete(component, false)); - - // Ensure everything has been deleted as expected. - assertThat(qm.getAllComponents(project)).isEmpty(); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, component.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, componentChild.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(DependencyMetrics.class, metrics.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(PolicyViolation.class, policyViolation.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(IntegrityAnalysis.class, integrityAnalysis.getId())); - - // Ensure associated objects were NOT deleted. - assertThatNoException().isThrownBy(() -> qm.getObjectById(Project.class, project.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(Vulnerability.class, vuln.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(PolicyCondition.class, policyCondition.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(Policy.class, policy.getId())); - } - @Test public void testGetComponentsByPurl() { final var project = new Project(); diff --git a/src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java index b50ee62a0..cd08b551e 100644 --- a/src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java +++ b/src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java @@ -18,191 +18,22 @@ */ package org.dependencytrack.persistence; -import alpine.notification.NotificationLevel; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.Analysis; -import org.dependencytrack.model.AnalysisJustification; -import org.dependencytrack.model.AnalysisResponse; -import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.AnalyzerIdentity; -import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; -import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.Finding; -import org.dependencytrack.model.IntegrityAnalysis; -import org.dependencytrack.model.IntegrityMatchStatus; -import org.dependencytrack.model.NotificationPublisher; -import org.dependencytrack.model.NotificationRule; -import org.dependencytrack.model.OrganizationalContact; -import org.dependencytrack.model.Policy; -import org.dependencytrack.model.PolicyCondition; -import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; -import org.dependencytrack.model.ProjectMetadata; -import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.Severity; -import org.dependencytrack.model.Vex; -import org.dependencytrack.model.ViolationAnalysis; -import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; -import org.dependencytrack.notification.NotificationScope; import org.junit.Test; -import javax.jdo.JDOObjectNotFoundException; import java.util.Date; import java.util.List; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNoException; public class ProjectQueryManagerTest extends PersistenceCapableTest { - @Test - public void recursivelyDeleteTest() { - final var project = new Project(); - project.setName("acme-app"); - project.setVersion("1.0.0"); - qm.persist(project); - - final var author = new OrganizationalContact(); - author.setName("authorName"); - final var projectMetadata = new ProjectMetadata(); - projectMetadata.setProject(project); - projectMetadata.setAuthors(List.of(author)); - qm.persist(projectMetadata); - - final var component = new Component(); - component.setProject(project); - component.setName("acme-lib"); - component.setVersion("2.0.0"); - qm.persist(component); - - // Assign a vulnerability and an accompanying analysis with comments to component. - final var vuln = new Vulnerability(); - vuln.setVulnId("INT-123"); - vuln.setSource(Vulnerability.Source.INTERNAL); - qm.persist(vuln); - qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); - final Analysis analysis = qm.makeAnalysis(component, vuln, - AnalysisState.NOT_AFFECTED, - AnalysisJustification.CODE_NOT_REACHABLE, - AnalysisResponse.WORKAROUND_AVAILABLE, - "analysisDetails", false); - qm.makeAnalysisComment(analysis, "someComment", "someCommenter"); - - // Create a child component to validate that deletion is indeed recursive. - final var componentChild = new Component(); - componentChild.setProject(project); - componentChild.setParent(component); - componentChild.setName("acme-sub-lib"); - componentChild.setVersion("3.0.0"); - qm.persist(componentChild); - - // Assign a policy violation and an accompanying analysis with comments to componentChild. - final var policy = new Policy(); - policy.setName("Test Policy"); - policy.setViolationState(Policy.ViolationState.WARN); - policy.setOperator(Policy.Operator.ALL); - policy.setProjects(List.of(project)); - qm.persist(policy); - final var policyCondition = new PolicyCondition(); - policyCondition.setPolicy(policy); - policyCondition.setSubject(PolicyCondition.Subject.COORDINATES); - policyCondition.setOperator(PolicyCondition.Operator.MATCHES); - policyCondition.setValue("someValue"); - qm.persist(policyCondition); - final var policyViolation = new PolicyViolation(); - policyViolation.setPolicyCondition(policyCondition); - policyViolation.setComponent(componentChild); - policyViolation.setType(PolicyViolation.Type.OPERATIONAL); - policyViolation.setTimestamp(new Date()); - qm.persist(policyViolation); - final ViolationAnalysis violationAnalysis = qm.makeViolationAnalysis(componentChild, policyViolation, - ViolationAnalysisState.REJECTED, false); - qm.makeViolationAnalysisComment(violationAnalysis, "someComment", "someCommenter"); - - // Assign am integrity analysis to componentChild - final var integrityAnalysis = new IntegrityAnalysis(); - integrityAnalysis.setComponent(componentChild); - integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha1HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha256HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setSha512HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); - integrityAnalysis.setUpdatedAt(new Date()); - qm.persist(integrityAnalysis); - - // Create metrics for component. - final var componentMetrics = new DependencyMetrics(); - componentMetrics.setProject(project); - componentMetrics.setComponent(component); - componentMetrics.setFirstOccurrence(new Date()); - componentMetrics.setLastOccurrence(new Date()); - qm.persist(componentMetrics); - - // Create metrics for project. - final var projectMetrics = new ProjectMetrics(); - projectMetrics.setProject(project); - projectMetrics.setFirstOccurrence(new Date()); - projectMetrics.setLastOccurrence(new Date()); - qm.persist(projectMetrics); - - // Create a BOM. - final Bom bom = qm.createBom(project, new Date(), Bom.Format.CYCLONEDX, "1.4", 1, "serialNumber", UUID.randomUUID(), null); - - // Create a child project with an accompanying component. - final var projectChild = new Project(); - projectChild.setParent(project); - projectChild.setName("acme-sub-app"); - projectChild.setVersion("1.1.0"); - qm.persist(projectChild); - final var projectChildComponent = new Component(); - projectChildComponent.setProject(projectChild); - projectChildComponent.setName("acme-lib-x"); - projectChildComponent.setVersion("4.0.0"); - qm.persist(projectChildComponent); - - // Create a VEX for projectChild. - final Vex vex = qm.createVex(projectChild, new Date(), Vex.Format.CYCLONEDX, "1.3", 1, "serialNumber"); - - // Create a notification rule and associate projectChild with it. - final NotificationPublisher notificationPublisher = qm.createNotificationPublisher("name", "description", "publisherClass", "templateContent", "templateMimeType", true); - final NotificationRule notificationRule = qm.createNotificationRule("name", NotificationScope.PORTFOLIO, NotificationLevel.WARNING, notificationPublisher); - notificationRule.getProjects().add(projectChild); - qm.persist(notificationRule); - - assertThatNoException() - .isThrownBy(() -> qm.recursivelyDelete(project, false)); - - // Ensure everything has been deleted as expected. - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Project.class, project.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Project.class, projectChild.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, component.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, componentChild.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, projectChildComponent.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(ProjectMetrics.class, projectMetrics.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(ProjectMetadata.class, projectMetadata.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(DependencyMetrics.class, componentMetrics.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(IntegrityAnalysis.class, integrityAnalysis.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Bom.class, bom.getId())); - assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Vex.class, vex.getId())); - - // Ensure associated objects were NOT deleted. - assertThatNoException().isThrownBy(() -> qm.getObjectById(Vulnerability.class, vuln.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(PolicyCondition.class, policyCondition.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(Policy.class, policy.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(NotificationRule.class, notificationRule.getId())); - assertThatNoException().isThrownBy(() -> qm.getObjectById(NotificationPublisher.class, notificationPublisher.getId())); - - // Ensure that associations have been cleaned up. - qm.getPersistenceManager().refresh(notificationRule); - assertThat(notificationRule.getProjects()).isEmpty(); - qm.getPersistenceManager().refresh(policy); - assertThat(policy.getProjects()).isEmpty(); - } - @Test public void testCloneProjectPreservesVulnerabilityAttributionDate() throws Exception { Project project = qm.createProject("Example Project 1", "Description 1", "1.0", null, null, null, null, false); diff --git a/src/test/java/org/dependencytrack/persistence/jdbi/ComponentDaoTest.java b/src/test/java/org/dependencytrack/persistence/jdbi/ComponentDaoTest.java new file mode 100644 index 000000000..7c64c581e --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/jdbi/ComponentDaoTest.java @@ -0,0 +1,162 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.jdbi; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Analysis; +import org.dependencytrack.model.AnalysisJustification; +import org.dependencytrack.model.AnalysisResponse; +import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.AnalyzerIdentity; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.DependencyMetrics; +import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMatchStatus; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ViolationAnalysis; +import org.dependencytrack.model.ViolationAnalysisState; +import org.dependencytrack.model.Vulnerability; +import org.jdbi.v3.core.Handle; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.jdo.JDOObjectNotFoundException; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + +public class ComponentDaoTest extends PersistenceCapableTest { + + private Handle jdbiHandle; + private ComponentDao componentDao; + + @Before + public void before() throws Exception { + super.before(); + jdbiHandle = openJdbiHandle(); + componentDao = jdbiHandle.attach(ComponentDao.class); + } + + @After + public void after() { + if (jdbiHandle != null) { + jdbiHandle.close(); + } + super.after(); + } + + @Test + public void testCascadeDeleteComponent() { + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("2.0.0"); + qm.persist(component); + + // Assign a vulnerability and an accompanying analysis with comments to component. + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-123"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); + final Analysis analysis = qm.makeAnalysis(component, vuln, + AnalysisState.NOT_AFFECTED, + AnalysisJustification.CODE_NOT_REACHABLE, + AnalysisResponse.WORKAROUND_AVAILABLE, + "analysisDetails", false); + qm.makeAnalysisComment(analysis, "someComment", "someCommenter"); + + // Create a child component to validate that deletion is indeed recursive. + final var componentChild = new Component(); + componentChild.setProject(project); + componentChild.setParent(component); + componentChild.setName("acme-sub-lib"); + componentChild.setVersion("3.0.0"); + qm.persist(componentChild); + + // Assign a policy violation and an accompanying analysis with comments to componentChild. + final var policy = new Policy(); + policy.setName("Test Policy"); + policy.setViolationState(Policy.ViolationState.WARN); + policy.setOperator(Policy.Operator.ALL); + qm.persist(policy); + final var policyCondition = new PolicyCondition(); + policyCondition.setPolicy(policy); + policyCondition.setSubject(PolicyCondition.Subject.COORDINATES); + policyCondition.setOperator(PolicyCondition.Operator.MATCHES); + policyCondition.setValue("someValue"); + qm.persist(policyCondition); + final var policyViolation = new PolicyViolation(); + policyViolation.setPolicyCondition(policyCondition); + policyViolation.setComponent(componentChild); + policyViolation.setType(PolicyViolation.Type.OPERATIONAL); + policyViolation.setTimestamp(new Date()); + qm.persist(policyViolation); + final ViolationAnalysis violationAnalysis = qm.makeViolationAnalysis(componentChild, policyViolation, + ViolationAnalysisState.REJECTED, false); + qm.makeViolationAnalysisComment(violationAnalysis, "someComment", "someCommenter"); + + // Assign am integrity analysis to componentChild + final var integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(componentChild); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha1HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha256HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha512HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setUpdatedAt(new Date()); + qm.persist(integrityAnalysis); + + // Create metrics for component. + final var metrics = new DependencyMetrics(); + metrics.setProject(project); + metrics.setComponent(component); + metrics.setFirstOccurrence(new Date()); + metrics.setLastOccurrence(new Date()); + qm.persist(metrics); + + componentDao.deleteComponent(component.getUuid()); + + // Ensure everything has been deleted as expected. + assertThat(qm.getAllComponents(project)).isEmpty(); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, component.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, componentChild.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(DependencyMetrics.class, metrics.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(PolicyViolation.class, policyViolation.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(IntegrityAnalysis.class, integrityAnalysis.getId())); + + // Ensure associated objects were NOT deleted. + assertThatNoException().isThrownBy(() -> qm.getObjectById(Project.class, project.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(Vulnerability.class, vuln.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(PolicyCondition.class, policyCondition.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(Policy.class, policy.getId())); + } +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/persistence/jdbi/ProjectDaoTest.java b/src/test/java/org/dependencytrack/persistence/jdbi/ProjectDaoTest.java new file mode 100644 index 000000000..fb79a982b --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/jdbi/ProjectDaoTest.java @@ -0,0 +1,231 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.jdbi; + +import alpine.notification.NotificationLevel; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Analysis; +import org.dependencytrack.model.AnalysisJustification; +import org.dependencytrack.model.AnalysisResponse; +import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.AnalyzerIdentity; +import org.dependencytrack.model.Bom; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.DependencyMetrics; +import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMatchStatus; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.OrganizationalContact; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; +import org.dependencytrack.model.ProjectMetrics; +import org.dependencytrack.model.ServiceComponent; +import org.dependencytrack.model.Vex; +import org.dependencytrack.model.ViolationAnalysis; +import org.dependencytrack.model.ViolationAnalysisState; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.notification.NotificationScope; +import org.jdbi.v3.core.Handle; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.jdo.JDOObjectNotFoundException; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + +public class ProjectDaoTest extends PersistenceCapableTest { + + private Handle jdbiHandle; + private ProjectDao projectDao; + + @Before + public void before() throws Exception { + super.before(); + jdbiHandle = openJdbiHandle(); + projectDao = jdbiHandle.attach(ProjectDao.class); + } + + @After + public void after() { + if (jdbiHandle != null) { + jdbiHandle.close(); + } + super.after(); + } + + @Test + public void testCascadeDeleteProject() { + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var author = new OrganizationalContact(); + author.setName("authorName"); + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setProject(project); + projectMetadata.setAuthors(List.of(author)); + qm.persist(projectMetadata); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("2.0.0"); + qm.persist(component); + + // Assign a vulnerability and an accompanying analysis with comments to component. + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-123"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); + final Analysis analysis = qm.makeAnalysis(component, vuln, + AnalysisState.NOT_AFFECTED, + AnalysisJustification.CODE_NOT_REACHABLE, + AnalysisResponse.WORKAROUND_AVAILABLE, + "analysisDetails", false); + qm.makeAnalysisComment(analysis, "someComment", "someCommenter"); + + // Create a child component to validate that deletion is indeed recursive. + final var componentChild = new Component(); + componentChild.setProject(project); + componentChild.setParent(component); + componentChild.setName("acme-sub-lib"); + componentChild.setVersion("3.0.0"); + qm.persist(componentChild); + + // Assign a policy violation and an accompanying analysis with comments to componentChild. + final var policy = new Policy(); + policy.setName("Test Policy"); + policy.setViolationState(Policy.ViolationState.WARN); + policy.setOperator(Policy.Operator.ALL); + policy.setProjects(List.of(project)); + qm.persist(policy); + final var policyCondition = new PolicyCondition(); + policyCondition.setPolicy(policy); + policyCondition.setSubject(PolicyCondition.Subject.COORDINATES); + policyCondition.setOperator(PolicyCondition.Operator.MATCHES); + policyCondition.setValue("someValue"); + qm.persist(policyCondition); + final var policyViolation = new PolicyViolation(); + policyViolation.setPolicyCondition(policyCondition); + policyViolation.setComponent(componentChild); + policyViolation.setType(PolicyViolation.Type.OPERATIONAL); + policyViolation.setTimestamp(new Date()); + qm.persist(policyViolation); + final ViolationAnalysis violationAnalysis = qm.makeViolationAnalysis(componentChild, policyViolation, + ViolationAnalysisState.REJECTED, false); + qm.makeViolationAnalysisComment(violationAnalysis, "someComment", "someCommenter"); + + // Assign am integrity analysis to componentChild + final var integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(componentChild); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha1HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha256HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha512HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setUpdatedAt(new Date()); + qm.persist(integrityAnalysis); + + // Create metrics for component. + final var componentMetrics = new DependencyMetrics(); + componentMetrics.setProject(project); + componentMetrics.setComponent(component); + componentMetrics.setFirstOccurrence(new Date()); + componentMetrics.setLastOccurrence(new Date()); + qm.persist(componentMetrics); + + // Create metrics for project. + final var projectMetrics = new ProjectMetrics(); + projectMetrics.setProject(project); + projectMetrics.setFirstOccurrence(new Date()); + projectMetrics.setLastOccurrence(new Date()); + qm.persist(projectMetrics); + + // Create a BOM. + final Bom bom = qm.createBom(project, new Date(), Bom.Format.CYCLONEDX, "1.4", 1, "serialNumber", UUID.randomUUID(), null); + + // Create a child project with an accompanying component. + final var projectChild = new Project(); + projectChild.setParent(project); + projectChild.setName("acme-sub-app"); + projectChild.setVersion("1.1.0"); + qm.persist(projectChild); + final var projectChildComponent = new Component(); + projectChildComponent.setProject(projectChild); + projectChildComponent.setName("acme-lib-x"); + projectChildComponent.setVersion("4.0.0"); + qm.persist(projectChildComponent); + + // Create a VEX for projectChild. + final Vex vex = qm.createVex(projectChild, new Date(), Vex.Format.CYCLONEDX, "1.3", 1, "serialNumber"); + + // Create a notification rule and associate projectChild with it. + final NotificationPublisher notificationPublisher = qm.createNotificationPublisher("name", "description", "publisherClass", "templateContent", "templateMimeType", true); + final NotificationRule notificationRule = qm.createNotificationRule("name", NotificationScope.PORTFOLIO, NotificationLevel.WARNING, notificationPublisher); + notificationRule.getProjects().add(projectChild); + qm.persist(notificationRule); + + final var serviceComponent = new ServiceComponent(); + serviceComponent.setName("service-component"); + serviceComponent.setProject(project); + qm.persist(serviceComponent); + + projectDao.deleteProject(project.getUuid()); + + // Ensure everything has been deleted as expected. + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Project.class, project.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Project.class, projectChild.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, component.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, componentChild.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Component.class, projectChildComponent.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(ProjectMetrics.class, projectMetrics.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(ProjectMetadata.class, projectMetadata.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(DependencyMetrics.class, componentMetrics.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(IntegrityAnalysis.class, integrityAnalysis.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Bom.class, bom.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(Vex.class, vex.getId())); + assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(ServiceComponent.class, serviceComponent.getId())); + + // Ensure associated objects were NOT deleted. + assertThatNoException().isThrownBy(() -> qm.getObjectById(Vulnerability.class, vuln.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(PolicyCondition.class, policyCondition.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(Policy.class, policy.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(NotificationRule.class, notificationRule.getId())); + assertThatNoException().isThrownBy(() -> qm.getObjectById(NotificationPublisher.class, notificationPublisher.getId())); + + // Ensure that associations have been cleaned up. + qm.getPersistenceManager().refresh(notificationRule); + assertThat(notificationRule.getProjects()).isEmpty(); + qm.getPersistenceManager().refresh(policy); + assertThat(policy.getProjects()).isEmpty(); + } +} \ No newline at end of file