From 297b192f9122afbf69635e2af0c7c3c28ef3e125 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 27 Nov 2024 19:51:50 +0100 Subject: [PATCH] Fix NPE when cloning projects with broken dependency graph Fixes #4413 Signed-off-by: nscuro --- .../persistence/ProjectQueryManager.java | 25 ++++++++++- .../resources/v1/ProjectResourceTest.java | 45 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index b462df02a4..d5c49ebe5a 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -711,7 +711,19 @@ public Project clone( String directDependencies = project.getDirectDependencies(); for (final UUID sourceComponentUuid : projectDirectDepsSourceComponentUuids) { final UUID clonedComponentUuid = clonedComponentUuidBySourceComponentUuid.get(sourceComponentUuid); - directDependencies = directDependencies.replace(sourceComponentUuid.toString(), clonedComponentUuid.toString()); + if (clonedComponentUuid != null) { + directDependencies = directDependencies.replace( + sourceComponentUuid.toString(), clonedComponentUuid.toString()); + } else { + // NB: This may happen when the source project itself is a clone, + // and it was cloned before DT v4.12.0. + // https://github.com/DependencyTrack/dependency-track/pull/4171 + LOGGER.warn(""" + The source project's directDependencies refer to a component with UUID \ + %s, which does not exist in the project. The cloned project's dependency graph \ + may be broken as a result. A BOM upload will resolve the issue.\ + """.formatted(sourceComponentUuid)); + } } project.setDirectDependencies(directDependencies); @@ -724,7 +736,16 @@ public Project clone( String directDependencies = component.getDirectDependencies(); for (final UUID sourceComponentUuid : sourceComponentUuids) { final UUID clonedComponentUuid = clonedComponentUuidBySourceComponentUuid.get(sourceComponentUuid); - directDependencies = directDependencies.replace(sourceComponentUuid.toString(), clonedComponentUuid.toString()); + if (clonedComponentUuid != null) { + directDependencies = directDependencies.replace( + sourceComponentUuid.toString(), clonedComponentUuid.toString()); + } else { + LOGGER.warn(""" + The directDependencies of component %s refer to a component with UUID \ + %s, which does not exist in the source project. The cloned project's dependency graph \ + may be broken as a result. A BOM upload will resolve the issue.\ + """.formatted(component, sourceComponentUuid)); + } } component.setDirectDependencies(directDependencies); diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 8c1f152c2f..f59282c72b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -1967,6 +1967,51 @@ public void cloneProjectAsLatestTest() { }); } + @Test // https://github.com/DependencyTrack/dependency-track/issues/4413 + public void cloneProjectWithBrokenDependencyGraphTest() { + EventService.getInstance().subscribe(CloneProjectEvent.class, CloneProjectTask.class); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + project.setDirectDependencies("[{\"uuid\":\"d6b6f140-f547-4fe2-a98c-f4942ad51f86\"}]"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("2.0.0"); + component.setDirectDependencies("[{\"uuid\":\"61503628-d2a2-447b-b99c-701b9d492cbd\"}]"); + qm.persist(component); + + final Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "project": "%s", + "version": "1.1.0", + "includeComponents": true, + "includeServices": true + } + """.formatted(project.getUuid()))); + assertThat(response.getStatus()).isEqualTo(200); + + await("Cloning completion") + .atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + final Project clonedProject = qm.getProject("acme-app", "1.1.0"); + assertThat(clonedProject).isNotNull(); + }); + + final Project clonedProject = qm.getProject("acme-app", "1.1.0"); + assertThat(clonedProject.getDirectDependencies()).isEqualTo( + "[{\"uuid\":\"d6b6f140-f547-4fe2-a98c-f4942ad51f86\"}]"); + + assertThat(qm.getAllComponents(clonedProject).getFirst().getDirectDependencies()).isEqualTo( + "[{\"uuid\":\"61503628-d2a2-447b-b99c-701b9d492cbd\"}]"); + } + @Test // https://github.com/DependencyTrack/dependency-track/issues/3883 public void issue3883RegressionTest() { Response response = jersey.target(V1_PROJECT)