diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 5538685d16..2513e50d17 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -34,7 +34,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import io.swagger.v3.oas.annotations.media.Schema; - import org.dependencytrack.parser.cyclonedx.util.ModelConverter; import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; @@ -104,11 +103,19 @@ }), @FetchGroup(name = "METRICS_UPDATE", members = { @Persistent(name = "id"), + @Persistent(name = "parent"), + @Persistent(name = "collectionLogic"), + @Persistent(name = "collectionTag"), @Persistent(name = "lastInheritedRiskScore"), @Persistent(name = "uuid") }), @FetchGroup(name = "PARENT", members = { @Persistent(name = "parent") + }), + @FetchGroup(name = "PORTFOLIO_METRICS_UPDATE", members = { + @Persistent(name = "id"), + @Persistent(name = "lastInheritedRiskScore"), + @Persistent(name = "uuid") }) }) @JsonInclude(JsonInclude.Include.NON_NULL) @@ -123,7 +130,8 @@ public enum FetchGroup { ALL, METADATA, METRICS_UPDATE, - PARENT + PARENT, + PORTFOLIO_METRICS_UPDATE } @PrimaryKey @@ -190,10 +198,9 @@ public enum FetchGroup { private Classifier classifier; @Persistent - @Column(name = "COLLECTION_LOGIC", jdbcType = "VARCHAR", allowsNull = "true", defaultValue = "NONE") // New column, must allow nulls on existing databases - @Index(name = "PROJECT_COLLECTION_LOGIC_IDX") + @Column(name = "COLLECTION_LOGIC", jdbcType = "VARCHAR", allowsNull = "true") @Extension(vendorName = "datanucleus", key = "enum-check-constraint", value = "true") - private ProjectCollectionLogic collectionLogic = ProjectCollectionLogic.NONE; + private ProjectCollectionLogic collectionLogic; @Persistent(defaultFetchGroup = "true") @Column(name = "COLLECTION_TAG", allowsNull = "true") @@ -405,13 +412,16 @@ public void setClassifier(Classifier classifier) { this.classifier = classifier; } - public ProjectCollectionLogic getCollectionLogic() { - return collectionLogic; + return collectionLogic == null + ? ProjectCollectionLogic.NONE + : collectionLogic; } public void setCollectionLogic(ProjectCollectionLogic collectionLogic) { - this.collectionLogic = collectionLogic; + this.collectionLogic = collectionLogic != ProjectCollectionLogic.NONE + ? collectionLogic + : null; } public Tag getCollectionTag() { diff --git a/src/main/java/org/dependencytrack/model/ProjectCollectionLogic.java b/src/main/java/org/dependencytrack/model/ProjectCollectionLogic.java index 3502d4c37d..b863084f71 100644 --- a/src/main/java/org/dependencytrack/model/ProjectCollectionLogic.java +++ b/src/main/java/org/dependencytrack/model/ProjectCollectionLogic.java @@ -25,7 +25,7 @@ * by this type. * * @author Ralf King - * @since 4.11.0 + * @since 4.13.0 */ public enum ProjectCollectionLogic { /** diff --git a/src/main/java/org/dependencytrack/model/ProjectMetrics.java b/src/main/java/org/dependencytrack/model/ProjectMetrics.java index ff7363ca2c..439bc62c04 100644 --- a/src/main/java/org/dependencytrack/model/ProjectMetrics.java +++ b/src/main/java/org/dependencytrack/model/ProjectMetrics.java @@ -172,12 +172,12 @@ public class ProjectMetrics implements Serializable { private Integer policyViolationsOperationalUnaudited; @Persistent - @Column(name = "COLLECTION_LOGIC", allowsNull = "true") // New column, must allow nulls on existing data bases + @Column(name = "COLLECTION_LOGIC", allowsNull = "true") private ProjectCollectionLogic collectionLogic; @Persistent - @Column(name = "COLLECTION_LOGIC_CHANGED", allowsNull = "true") // New column, must allow nulls on existing data bases - private Boolean collectionLogicChanged = false; + @Column(name = "COLLECTION_LOGIC_CHANGED", allowsNull = "false", defaultValue = "false") + private boolean collectionLogicChanged = false; @Persistent @Column(name = "FIRST_OCCURRENCE", allowsNull = "false") @@ -433,16 +433,23 @@ public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperatio this.policyViolationsOperationalUnaudited = policyViolationsOperationalUnaudited; } - public ProjectCollectionLogic getCollectionLogic() { return collectionLogic; } + public ProjectCollectionLogic getCollectionLogic() { + return collectionLogic == null + ? ProjectCollectionLogic.NONE + : collectionLogic; + } public void setCollectionLogic(ProjectCollectionLogic collectionLogic) { - // convert old NULL values from DB to NONE - this.collectionLogic = collectionLogic != null ? collectionLogic : ProjectCollectionLogic.NONE; + this.collectionLogic = collectionLogic != ProjectCollectionLogic.NONE + ? collectionLogic + : null; } - public Boolean isCollectionLogicChanged() { return collectionLogicChanged; } + public boolean isCollectionLogicChanged() { + return collectionLogicChanged; + } - public void setCollectionLogicChanged(Boolean collectionLogicChanged) { + public void setCollectionLogicChanged(boolean collectionLogicChanged) { this.collectionLogicChanged = collectionLogicChanged; } diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 10a51d8a6e..31d2e98128 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -596,11 +596,6 @@ public Project updateProject(Project transientProject, boolean commitIndex) { } else { project.setCollectionTag(null); } - // Force loading parent. This seems useless but somehow the code block above magically unloads the parent, - // making it missing in the API response. Following line enforces it to be available again. - // For reference see following Unit Test which would fail without this: - // org.dependencytrack.resources.v1.ProjectResourceTest.patchProjectParentTest - project.getParent(); return persist(project); }); diff --git a/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java index da40183579..edb2ac5491 100644 --- a/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java +++ b/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java @@ -189,7 +189,7 @@ private List fetchNextActiveProjectsBatch(final PersistenceManager pm, // NB: Set fetch group on PM level to avoid fields of the default fetch group from being loaded. try (var ignoredPersistenceCustomization = new ScopedCustomization(pm) - .withFetchGroup(Project.FetchGroup.METRICS_UPDATE.name())) { + .withFetchGroup(Project.FetchGroup.PORTFOLIO_METRICS_UPDATE.name())) { return List.copyOf(query.executeList()); } finally { query.closeAll(); diff --git a/src/main/java/org/dependencytrack/tasks/metrics/ProjectMetricsUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/ProjectMetricsUpdateTask.java index d8305d8208..a8dae87168 100644 --- a/src/main/java/org/dependencytrack/tasks/metrics/ProjectMetricsUpdateTask.java +++ b/src/main/java/org/dependencytrack/tasks/metrics/ProjectMetricsUpdateTask.java @@ -62,13 +62,13 @@ public void inform(final Event e) { } } - private void updateMetrics(final UUID uuid) throws Exception { + private void updateMetrics(final UUID uuid) { final var counters = new Counters(); try (final QueryManager qm = new QueryManager()) { final PersistenceManager pm = qm.getPersistenceManager(); - final Project project = qm.getObjectByUuid(Project.class, uuid, List.of(Project.FetchGroup.METRICS_UPDATE.name())); + final Project project = fetchProject(pm, uuid); if (project == null) { throw new NoSuchElementException("Project " + uuid + " does not exist"); } @@ -115,7 +115,7 @@ private void updateMetrics(final UUID uuid) throws Exception { } } - private void updateRegularProjectMetrics(final Project project, final PersistenceManager pm, final Counters counters) throws Exception { + private void updateRegularProjectMetrics(final Project project, final PersistenceManager pm, final Counters counters) { final UUID uuid = project.getUuid(); LOGGER.debug("Fetching first components page for project " + uuid); @@ -240,13 +240,10 @@ private void updateAggregateDirectChildrenWithTagCollectionMetrics(final Project } private void updateLatestVersionChildrenCollectionMetrics(final Project project, final PersistenceManager pm, final Counters counters) { - LOGGER.warn("Collection logic LATEST_VERSION_CHILDREN not yet implemented. Waiting for https://github.com/DependencyTrack/dependency-track/issues/4148"); - /* - TODO: Create Test case in ProjectMetricsUpdateTaskTest*/ LOGGER.debug("Fetching metrics of children of collection project " + project.getUuid() + " using collection logic " + project.getCollectionLogic()); - Query subQuery = pm.newQuery(ProjectMetrics.class); + Query subQuery = pm.newQuery(ProjectMetrics.class); subQuery.setFilter("project == :project"); subQuery.setResult("max(lastOccurrence)"); @@ -266,6 +263,20 @@ private void updateLatestVersionChildrenCollectionMetrics(final Project project, } } + private Project fetchProject(final PersistenceManager pm, final UUID uuid) { + final Query query = pm.newQuery(Project.class); + query.setFilter("uuid == :uuid"); + query.setParameters(uuid); + + // NB: Set fetch group on PM level to avoid fields of the default fetch group from being loaded. + try (var ignoredPersistenceCustomization = new ScopedCustomization(pm) + .withFetchGroup(Project.FetchGroup.METRICS_UPDATE.name())) { + return query.executeUnique(); + } finally { + query.closeAll(); + } + } + private List fetchNextComponentsPage(final PersistenceManager pm, final Project project, final Long lastId) { final Query query = pm.newQuery(Component.class); if (lastId == null) { @@ -293,7 +304,7 @@ private void addToCounters(Counters counters, ProjectMetrics projectMetrics) { counters.medium += projectMetrics.getMedium(); counters.low += projectMetrics.getLow(); counters.unassigned += projectMetrics.getUnassigned(); - counters.vulnerabilities += projectMetrics.getVulnerabilities(); + counters.vulnerabilities += Math.toIntExact(projectMetrics.getVulnerabilities()); counters.findingsTotal += projectMetrics.getFindingsTotal(); counters.findingsAudited += projectMetrics.getFindingsAudited(); diff --git a/src/main/java/org/dependencytrack/upgrade/UpgradeItems.java b/src/main/java/org/dependencytrack/upgrade/UpgradeItems.java index 924777c4d4..8fa30e9b58 100644 --- a/src/main/java/org/dependencytrack/upgrade/UpgradeItems.java +++ b/src/main/java/org/dependencytrack/upgrade/UpgradeItems.java @@ -41,7 +41,6 @@ class UpgradeItems { UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4110.v4110Updater.class); UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4120.v4120Updater.class); UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4122.v4122Updater.class); - UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4130.v4130Updater.class); } static List> getUpgradeItems() { diff --git a/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java b/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java deleted file mode 100644 index 2154d415aa..0000000000 --- a/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.upgrade.v4130; - -import alpine.common.logging.Logger; -import alpine.persistence.AlpineQueryManager; -import alpine.server.upgrade.AbstractUpgradeItem; - -import java.sql.Connection; -import java.sql.Statement; - -public class v4130Updater extends AbstractUpgradeItem { - - private static final Logger LOGGER = Logger.getLogger(v4130Updater.class); - - @Override - public String getSchemaVersion() { - return "4.13.0"; - } - - @Override - public void executeUpgrade(final AlpineQueryManager qm, final Connection connection) throws Exception { - setProjectCollectionLogicDefault(connection); - } - private static void setProjectCollectionLogicDefault(final Connection connection) throws Exception { - LOGGER.info("Setting collection logic of all projects to default NONE"); - try (final Statement stmt = connection.createStatement()) { - stmt.execute("UPDATE \"PROJECT\" SET \"COLLECTION_LOGIC\" = 'NONE'"); - } - } - -} diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 4a039a62b2..f48b0ed543 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -1221,6 +1221,60 @@ public void updateProjectAsLatestWithACLAndNoAccessTest() { Assert.assertTrue(qm.getProject(noAccessLatestProject.getName(), noAccessLatestProject.getVersion()).isLatest()); } + @Test + public void updateProjectToCollectionProjectWhenHavingComponentsTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "%s", + "name": "acme-app", + "collectionLogic": "AGGREGATE_DIRECT_CHILDREN" + } + """.formatted(project.getUuid()))); + assertThat(response.getStatus()).isEqualTo(409); + assertThat(getPlainTextBody(response)).isEqualTo(""" + Project cannot be made a collection project while it has \ + components or services!"""); + } + + @Test + public void updateProjectToCollectionProjectWhenHavingServicesTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var service = new ServiceComponent(); + service.setProject(project); + service.setName("some-service"); + qm.persist(service); + + final Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "%s", + "name": "acme-app", + "collectionLogic": "AGGREGATE_DIRECT_CHILDREN" + } + """.formatted(project.getUuid()))); + assertThat(response.getStatus()).isEqualTo(409); + assertThat(getPlainTextBody(response)).isEqualTo(""" + Project cannot be made a collection project while it has \ + components or services!"""); + } + @Test public void deleteProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false);