From bdacaef8a784e775bdd1c7d1bf206c134ff6f766 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 12 May 2026 18:29:53 +0200 Subject: [PATCH 1/4] test(integrations): add tests for defectdojo.groupBy per-project property Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../defectdojo/DefectDojoUploaderTest.java | 26 ++- .../tasks/DefectDojoUploadTaskTest.java | 178 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index c8c1e207f9..e497822878 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -93,4 +93,28 @@ void testIntegrationDisabledCases() { Assertions.assertFalse(extension.isProjectConfigured(project)); } -} \ No newline at end of file + @Test + void testGetGroupByReturnsNullWhenNotConfigured() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(httpClient, secretManager); + extension.setQueryManager(qm); + Assertions.assertNull(extension.getGroupBy(project)); + } + + @Test + void testGetGroupByReturnsValueWhenConfigured() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.groupBy", + "component_name", + IConfigProperty.PropertyType.STRING, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(httpClient, secretManager); + extension.setQueryManager(qm); + Assertions.assertEquals("component_name", extension.getGroupBy(project)); + } + +} diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java index f5e0516d15..649d2616af 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java @@ -931,6 +931,184 @@ void testUploadWithReimportAndNoExistingTest(WireMockRuntimeInfo wmRuntimeInfo) """, true, false)))); } + @Test + void testUploadWithGroupBy() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wmRuntimeInfo.getHttpBaseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + DEFECTDOJO_REIMPORT_ENABLED.getDefaultPropertyValue(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(post(urlPathEqualTo("/api/v2/import-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + 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("1.2.3"); + qm.persist(component); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + qm.createProjectProperty(project, "integrations", "defectdojo.groupBy", + "component_name", IConfigProperty.PropertyType.STRING, null); + + new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract()); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("engagement") + .withBody(equalTo("666"))) + .withAnyRequestBodyPart(aMultipart() + .withName("group_by") + .withBody(equalTo("component_name")))); + } + + @Test + void testUploadWithReimportAndGroupBy() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wmRuntimeInfo.getHttpBaseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + "false", + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(get(urlPathEqualTo("/api/v2/tests/")) + .withQueryParam("engagement", equalTo("666")) + .withQueryParam("limit", equalTo("100")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(""" + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "tags": [], + "test_type_name": "Dependency Track Finding Packaging Format (FPF) Export", + "finding_groups": [], + "scan_type": "Dependency Track Finding Packaging Format (FPF) Export", + "title": null, + "description": null, + "target_start": "2023-04-29T00:00:00Z", + "target_end": "2023-04-29T21:39:21.513481Z", + "estimated_time": null, + "actual_time": null, + "percent_complete": 100, + "updated": "2023-04-29T21:39:21.617857Z", + "created": "2023-04-29T21:39:21.516216Z", + "version": "", + "build_id": "", + "commit_hash": "", + "branch_tag": "", + "engagement": 666, + "lead": 1, + "test_type": 63, + "environment": 7, + "api_scan_configuration": null, + "notes": [], + "files": [] + } + ], + "prefetch": {} + } + """))); + + stubFor(post(urlPathEqualTo("/api/v2/reimport-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + 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("1.2.3"); + qm.persist(component); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + qm.createProjectProperty(project, "integrations", "defectdojo.reimport", + "true", IConfigProperty.PropertyType.BOOLEAN, null); + qm.createProjectProperty(project, "integrations", "defectdojo.groupBy", + "component_name+component_version", IConfigProperty.PropertyType.STRING, null); + + new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract()); + + verify(1, getRequestedFor(urlPathEqualTo("/api/v2/tests/"))); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("engagement") + .withBody(equalTo("666"))) + .withAnyRequestBodyPart(aMultipart() + .withName("test") + .withBody(equalTo("1"))) + .withAnyRequestBodyPart(aMultipart() + .withName("group_by") + .withBody(equalTo("component_name+component_version")))); + } + /** * Un-ignore this test to test the integration against a local DefectDojo deployment. *

From 0a599dbfef523524d956faef109d2705f04fe26c Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 12 May 2026 18:30:02 +0200 Subject: [PATCH 2/4] feat(integrations): add defectdojo.groupBy per-project property When set, forwards the value as group_by in the DefectDojo import-scan and reimport-scan multipart form requests, allowing findings to be grouped into Finding Groups on import. When not set, behavior is unchanged (backwards compatible). Closes #6061 Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../defectdojo/DefectDojoClient.java | 12 +++++++++-- .../defectdojo/DefectDojoUploader.java | 20 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 3f1b7a89b3..edbcccf3b3 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -60,7 +60,8 @@ public void uploadDependencyTrackFindings( final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, - final @Nullable String testTitle) { + final @Nullable String testTitle, + final @Nullable String groupBy) { LOGGER.debug("Uploading Dependency-Track findings to DefectDojo"); final var multipart = new MultipartBodyPublisher() @@ -76,6 +77,9 @@ public void uploadDependencyTrackFindings( if (testTitle != null) { multipart.addFormField("test_title", testTitle); } + if (groupBy != null) { + multipart.addFormField("group_by", groupBy); + } final var request = HttpRequest.newBuilder() .uri(URI.create(baseURL + "/api/v2/import-scan/")) @@ -187,7 +191,8 @@ public void reimportDependencyTrackFindings( final String testId, final Boolean doNotReactivate, final Boolean verifyFindings, - final @Nullable String testTitle) { + final @Nullable String testTitle, + final @Nullable String groupBy) { LOGGER.debug("Re-reimport Dependency-Track findings to DefectDojo per Engagement"); final var multipart = new MultipartBodyPublisher() @@ -205,6 +210,9 @@ public void reimportDependencyTrackFindings( if (testTitle != null) { multipart.addFormField("test_title", testTitle); } + if (groupBy != null) { + multipart.addFormField("group_by", groupBy); + } final var request = HttpRequest.newBuilder() .uri(URI.create(baseURL + "/api/v2/reimport-scan/")) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index f0034bb8d1..a0a1fcecd5 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -53,6 +53,7 @@ public class DefectDojoUploader extends AbstractIntegrationPoint implements Proj private static final String DO_NOT_REACTIVATE_PROPERTY = "defectdojo.doNotReactivate"; private static final String VERIFIED_PROPERTY = "defectdojo.verified"; private static final String TEST_TITLE_PROPERTY = "defectdojo.testTitle"; + private static final String GROUP_BY_PROPERTY = "defectdojo.groupBy"; private final HttpClient httpClient; private final SecretManager secretManager; @@ -98,6 +99,14 @@ private boolean isVerifiedConfigured(final Project project) { return null; } + public String getGroupBy(final Project project) { + final ProjectProperty groupBy = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), GROUP_BY_PROPERTY); + if (groupBy != null && groupBy.getPropertyValue() != null) { + return groupBy.getPropertyValue(); + } + return null; + } + @Override public String name() { return "DefectDojo"; @@ -142,6 +151,8 @@ public void upload(final Project project, final InputStream payload) { final boolean globalReimportEnabled = qm.isEnabled(DEFECTDOJO_REIMPORT_ENABLED); final ProjectProperty engagementId = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), ENGAGEMENTID_PROPERTY); final boolean verifyFindings = isVerifiedConfigured(project); + final String testTitle = getTestTitle(project); + final String groupBy = getGroupBy(project); try { final String apiKeyValue = secretManager.getSecretValue(apiKeySecretName); if (apiKeyValue == null) { @@ -160,7 +171,8 @@ public void upload(final Project project, final InputStream payload) { engagementId.getPropertyValue(), payload, verifyFindings, - testTitle); + testTitle, + groupBy); } else { client.reimportDependencyTrackFindings( apiKeyValue, @@ -169,7 +181,8 @@ public void upload(final Project project, final InputStream payload) { testId, isDoNotReactivateConfigured(project), verifyFindings, - testTitle); + testTitle, + groupBy); } } else { client.uploadDependencyTrackFindings( @@ -177,7 +190,8 @@ public void upload(final Project project, final InputStream payload) { engagementId.getPropertyValue(), payload, verifyFindings, - testTitle); + testTitle, + groupBy); } } catch (Exception e) { LOGGER.error("An error occurred attempting to upload findings to DefectDojo", e); From a9ce4fc681e869dcf60eacff7ec79fe2afd20010 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:34:08 +0200 Subject: [PATCH 3/4] fix(defectdojo): remove duplicate testTitle, fix getGroupBy visibility, adapt group_by tests to v5 API Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../defectdojo/DefectDojoUploader.java | 3 +-- .../tasks/DefectDojoUploadTaskTest.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index a0a1fcecd5..477fbae8f0 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -99,7 +99,7 @@ private boolean isVerifiedConfigured(final Project project) { return null; } - public String getGroupBy(final Project project) { + private @Nullable String getGroupBy(final Project project) { final ProjectProperty groupBy = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), GROUP_BY_PROPERTY); if (groupBy != null && groupBy.getPropertyValue() != null) { return groupBy.getPropertyValue(); @@ -159,7 +159,6 @@ public void upload(final Project project, final InputStream payload) { LOGGER.warn("DefectDojo API key secret '%s' could not be resolved. Aborting".formatted(apiKeySecretName)); return; } - final String testTitle = getTestTitle(project); final DefectDojoClient client = new DefectDojoClient(httpClient, this, URI.create(defectDojoUrl.getPropertyValue()).toURL()); if (isReimportConfigured(project) || globalReimportEnabled) { final ArrayList testsIds = client.getDojoTestIds(apiKeyValue, engagementId.getPropertyValue()); diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java index 649d2616af..bd30be8e08 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java @@ -932,7 +932,7 @@ void testUploadWithReimportAndNoExistingTest(WireMockRuntimeInfo wmRuntimeInfo) } @Test - void testUploadWithGroupBy() { + void testUploadWithGroupBy(WireMockRuntimeInfo wmRuntimeInfo) { qm.createConfigProperty( DEFECTDOJO_ENABLED.getGroupName(), DEFECTDOJO_ENABLED.getPropertyName(), @@ -950,7 +950,7 @@ void testUploadWithGroupBy() { qm.createConfigProperty( DEFECTDOJO_API_KEY.getGroupName(), DEFECTDOJO_API_KEY.getPropertyName(), - "dojoApiKey", + "apiKeySecretName", DEFECTDOJO_API_KEY.getPropertyType(), null ); @@ -982,7 +982,10 @@ void testUploadWithGroupBy() { qm.createProjectProperty(project, "integrations", "defectdojo.groupBy", "component_name", IConfigProperty.PropertyType.STRING, null); - new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract()); + new DefectDojoUploadTask( + HttpClient.newHttpClient(), + new TestSecretManager(Map.of("apiKeySecretName", "dojoApiKey"))) + .run(); verify(postRequestedFor(urlPathEqualTo("/api/v2/import-scan/")) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) @@ -995,7 +998,7 @@ void testUploadWithGroupBy() { } @Test - void testUploadWithReimportAndGroupBy() { + void testUploadWithReimportAndGroupBy(WireMockRuntimeInfo wmRuntimeInfo) { qm.createConfigProperty( DEFECTDOJO_ENABLED.getGroupName(), DEFECTDOJO_ENABLED.getPropertyName(), @@ -1013,7 +1016,7 @@ void testUploadWithReimportAndGroupBy() { qm.createConfigProperty( DEFECTDOJO_API_KEY.getGroupName(), DEFECTDOJO_API_KEY.getPropertyName(), - "dojoApiKey", + "apiKeySecretName", DEFECTDOJO_API_KEY.getPropertyType(), null ); @@ -1092,7 +1095,10 @@ void testUploadWithReimportAndGroupBy() { qm.createProjectProperty(project, "integrations", "defectdojo.groupBy", "component_name+component_version", IConfigProperty.PropertyType.STRING, null); - new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract()); + new DefectDojoUploadTask( + HttpClient.newHttpClient(), + new TestSecretManager(Map.of("apiKeySecretName", "dojoApiKey"))) + .run(); verify(1, getRequestedFor(urlPathEqualTo("/api/v2/tests/"))); From 99255b608afbcf3454b63b0abe3cb4255b692d88 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:27:36 +0200 Subject: [PATCH 4/4] Fix getGroupBy visibility and createProject arg in tests Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../integrations/defectdojo/DefectDojoUploader.java | 2 +- .../integrations/defectdojo/DefectDojoUploaderTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index 477fbae8f0..97398ee637 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -99,7 +99,7 @@ private boolean isVerifiedConfigured(final Project project) { return null; } - private @Nullable String getGroupBy(final Project project) { + @Nullable String getGroupBy(final Project project) { final ProjectProperty groupBy = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), GROUP_BY_PROPERTY); if (groupBy != null && groupBy.getPropertyValue() != null) { return groupBy.getPropertyValue(); diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index e497822878..9ecad72003 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -95,7 +95,7 @@ void testIntegrationDisabledCases() { @Test void testGetGroupByReturnsNullWhenNotConfigured() { - Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, null, false); DefectDojoUploader extension = new DefectDojoUploader(httpClient, secretManager); extension.setQueryManager(qm); Assertions.assertNull(extension.getGroupBy(project)); @@ -103,7 +103,7 @@ void testGetGroupByReturnsNullWhenNotConfigured() { @Test void testGetGroupByReturnsValueWhenConfigured() { - Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, null, false); qm.createProjectProperty( project, DEFECTDOJO_ENABLED.getGroupName(),