From 674e85ce7d28fb6e85f07d4b8cc78071956eff0a Mon Sep 17 00:00:00 2001 From: syalioune Date: Mon, 15 May 2023 19:55:33 +0200 Subject: [PATCH] Fix: Detect and avoid infinite recursion during second upload of SBOM with nested duplicate See #1905 for details Signed-off-by: syalioune --- .../parser/cyclonedx/util/ModelConverter.java | 7 ++-- .../tasks/BomUploadProcessingTaskTest.java | 24 +++++++++++++ src/test/resources/bom-2.json | 35 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/bom-2.json 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..118e090411 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -23,6 +23,7 @@ import com.github.packageurl.PackageURL; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; import org.cyclonedx.model.Bom; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Hash; @@ -96,7 +97,8 @@ public static List convertComponents(final QueryManager qm, final Bom @SuppressWarnings("deprecation") public static Component convert(final QueryManager qm, final org.cyclonedx.model.Component cycloneDxComponent, final Project project) { - Component component = qm.matchSingleIdentity(project, new ComponentIdentity(cycloneDxComponent)); + ComponentIdentity parentIdentity = new ComponentIdentity(cycloneDxComponent); + Component component = qm.matchSingleIdentity(project, parentIdentity); if (component == null) { component = new Component(); component.setProject(project); @@ -187,7 +189,8 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model final Collection components = new ArrayList<>(); for (int i = 0; i < cycloneDxComponent.getComponents().size(); i++) { final org.cyclonedx.model.Component cycloneDxChildComponent = cycloneDxComponent.getComponents().get(i); - if (cycloneDxChildComponent != null) { + ComponentIdentity childIdentity = new ComponentIdentity(cycloneDxChildComponent); + if (cycloneDxChildComponent != null && !EqualsBuilder.reflectionEquals(parentIdentity, childIdentity)) { components.add(convert(qm, cycloneDxChildComponent, project)); } } diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 55c866faec..739c1a8b8d 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -52,6 +52,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; @@ -209,4 +210,27 @@ public void informWithInvalidCycloneDxBomTest() throws Exception { assertThat(project.getLastBomImport()).isNull(); } + @Test + public void informTestWithInvalidCycloneDxBomRecursiveDuplicateTest() throws Exception { + // Test case for issue #1905 : https://github.com/DependencyTrack/dependency-track/issues/1905 + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + final byte[] bomBytes = Files.readAllBytes(Paths.get(getClass().getClassLoader().getResource("bom-2.json").toURI())); + + new BomUploadProcessingTask().inform(new BomUploadEvent(project.getUuid(), bomBytes)); + assertConditionWithTimeout(() -> qm.getAllComponents(project).size() == 1, Duration.ofSeconds(5)); + assertConditionWithTimeout(() -> NOTIFICATIONS.size() >= 3, Duration.ofSeconds(5)); + List components = qm.getAllComponents(project); + final Component component = components.get(0); + assertThat(component.getName()).isEqualTo("Pillow"); + assertThat(component.getVersion()).isEqualTo("9.3.0"); + assertThat(component.getCpe()).isEqualTo("cpe:2.3:a:alex_clark_\\(pil_fork_author\\):python-Pillow:9.3.0:*:*:*:*:*:*:*"); + assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:pypi/pillow@9.3.0"); + assertThat(NOTIFICATIONS).satisfiesExactly( + n -> assertThat(n.getGroup()).isEqualTo(NotificationGroup.PROJECT_CREATED.name()), + n -> assertThat(n.getGroup()).isEqualTo(NotificationGroup.BOM_CONSUMED.name()), + n -> assertThat(n.getGroup()).isEqualTo(NotificationGroup.BOM_PROCESSED.name()) + ); + } + } diff --git a/src/test/resources/bom-2.json b/src/test/resources/bom-2.json new file mode 100644 index 0000000000..43b2653658 --- /dev/null +++ b/src/test/resources/bom-2.json @@ -0,0 +1,35 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-01-01T11:01:51Z", + "tools": [ + { + "vendor": "changeme", + "name": "changeme", + "version": "0.62.3" + } + ] + }, + "components": [ + { + "bom-ref": "pkg:pypi/pillow@9.3.0", + "type": "library", + "name": "Pillow", + "version": "9.3.0", + "cpe": "cpe:2.3:a:alex_clark_\\(pil_fork_author\\):python-Pillow:9.3.0:*:*:*:*:*:*:*", + "purl": "pkg:pypi/Pillow@9.3.0", + "components": [ + { + "bom-ref": "pkg:pypi/pillow@9.3.0?package-id=212c649613e17901", + "type": "library", + "name": "Pillow", + "version": "9.3.0", + "cpe": "cpe:2.3:a:alex_clark_\\(pil_fork_author\\):python-Pillow:9.3.0:*:*:*:*:*:*:*", + "purl": "pkg:pypi/Pillow@9.3.0" + } + ] + } + ] +} \ No newline at end of file