From e41244d520011253507b992b508710e966387b11 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/5] test(integrations): add tests for defectdojo.groupBy per-project property Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../defectdojo/DefectDojoUploaderTest.java | 24 +++ .../tasks/DefectDojoUploadTaskTest.java | 178 ++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index ace125478a..c1104dfd0a 100644 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -68,4 +68,28 @@ void testIntegrationDisabledCases() { Assertions.assertFalse(extension.isProjectConfigured(project)); } + @Test + void testGetGroupByReturnsNullWhenNotConfigured() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + 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(); + extension.setQueryManager(qm); + Assertions.assertEquals("component_name", extension.getGroupBy(project)); + } + } diff --git a/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java b/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java index c5c60c62a0..021041568d 100644 --- a/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java @@ -909,6 +909,184 @@ void testUploadWithReimportAndNoExistingTest() { """, 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 0ed88a43aeefcf827f6961f38f31f11e17f0caef 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/5] 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 | 14 ++++++++++---- .../defectdojo/DefectDojoUploader.java | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 340eeb00b1..dc5f972939 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -55,7 +55,7 @@ public DefectDojoClient(final DefectDojoUploader uploader, final URL baseURL) { this.baseURL = baseURL; } - public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle) { + public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle, final String groupBy) { LOGGER.debug("Uploading Dependency-Track findings to DefectDojo"); HttpPost request = new HttpPost(baseURL + "/api/v2/import-scan/"); InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json"); @@ -72,9 +72,12 @@ public void uploadDependencyTrackFindings(final String token, final String engag .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) .addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA)) .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)); - if(testTitle != null) { + if (testTitle != null) { builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA)); } + if (groupBy != null) { + builder.addPart("group_by", new StringBody(groupBy, ContentType.MULTIPART_FORM_DATA)); + } request.setEntity(builder.build()); try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { @@ -164,7 +167,7 @@ public ArrayList jsonToList(final JSONArray jsonArray) { * A Reimport will reuse (overwrite) the existing test, instead of create a new test. * The Successfully reimport will also increase the reimport counter by 1. */ - public void reimportDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final String testId, final Boolean doNotReactivate, final Boolean verifyFindings, final String testTitle) { + public void reimportDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final String testId, final Boolean doNotReactivate, final Boolean verifyFindings, final String testTitle, final String groupBy) { LOGGER.debug("Re-reimport Dependency-Track findings to DefectDojo per Engagement"); HttpPost request = new HttpPost(baseURL + "/api/v2/reimport-scan/"); request.addHeader("accept", "application/json"); @@ -184,9 +187,12 @@ public void reimportDependencyTrackFindings(final String token, final String eng .addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA)) .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) .build(); - if(testTitle != null) { + if (testTitle != null) { builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA)); } + if (groupBy != null) { + builder.addPart("group_by", new StringBody(groupBy, ContentType.MULTIPART_FORM_DATA)); + } request.setEntity(builder.build()); try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index ab3a169287..923ef95a01 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -47,6 +47,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"; public boolean isReimportConfigured(final Project project) { final ProjectProperty reimport = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), REIMPORT_PROPERTY); @@ -84,6 +85,14 @@ public String getTestTitle(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"; @@ -119,19 +128,21 @@ 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 DefectDojoClient client = new DefectDojoClient(this, URI.create(defectDojoUrl.getPropertyValue()).toURL()); if (isReimportConfigured(project) || globalReimportEnabled) { final ArrayList testsIds = client.getDojoTestIds(apiKey.getPropertyValue(), engagementId.getPropertyValue()); - final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds, getTestTitle(project)); + final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds, testTitle); LOGGER.debug("Found existing test Id: " + testId); if (testId.equals("")) { - client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, testTitle, groupBy); } else { - client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, getTestTitle(project)); + client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, testTitle, groupBy); } } else { - client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, testTitle, groupBy); } } catch (Exception e) { LOGGER.error("An error occurred attempting to upload findings to DefectDojo", e); From 5c6a7680ea39072d587949970fb8023fa095273f Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 12 May 2026 19:41:02 +0200 Subject: [PATCH 3/5] fix(integrations): remove spurious build() call in reimportDependencyTrackFindings Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- .../integrations/defectdojo/DefectDojoClient.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index dc5f972939..667c707db8 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -185,8 +185,7 @@ public void reimportDependencyTrackFindings(final String token, final String eng .addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA)) .addPart("do_not_reactivate", new StringBody(doNotReactivate.toString(), ContentType.MULTIPART_FORM_DATA)) .addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA)) - .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) - .build(); + .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)); if (testTitle != null) { builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA)); } From c03d49706bb6908a56a60319a49e0b8443897a18 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Wed, 13 May 2026 18:20:19 +0200 Subject: [PATCH 4/5] Updated the DefectDojo integration documentation Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> --- docs/_docs/integrations/defectdojo.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/_docs/integrations/defectdojo.md b/docs/_docs/integrations/defectdojo.md index ce8a130a01..d29d3cf136 100644 --- a/docs/_docs/integrations/defectdojo.md +++ b/docs/_docs/integrations/defectdojo.md @@ -101,3 +101,25 @@ The DefectDojo documentation says 'If no test_title is provided, the latest test * Dependency-Track v4.6.0 or higher ![Configure Project](/images/screenshots/defectdojo_global_reimport.png) Alternatively, you can turn on the above reimport feature for all projects in one click, by checking on 'Enable reimport' box as shown in the screenshot above. + +### Step 11: Add per project configuration for finding groups + +* Not supported in any release yet + +You can define how findings should be grouped into Finding Groups in DefectDojo on import/reimport. This is particularly useful when pushing findings to an issue tracker like Jira, as it allows multiple related findings (e.g. all vulnerabilities in the same component) to be consolidated into a single ticket instead of one ticket per finding. + +If this property is not set, no grouping is applied and DefectDojo's default behavior is used. + +Supported values are defined by DefectDojo (see [`GROUP_BY_OPTIONS`](https://github.com/DefectDojo/django-DefectDojo/blob/6eab87386d504c4bc164f87b6aae58a8e0c1b8d2/dojo/models.py#L3703) in the DefectDojo source), and currently include: + +* `component_name` +* `component_name+component_version` +* `file_path` +* `finding_title` + +| Attribute | Value | +|----------------|---------------------------------------------------------------| +| Group Name | `integrations` | +| Property Name | `defectdojo.groupBy` | +| Property Value | One of the supported `group_by` values, e.g. `component_name` | +| Property Type | `STRING` | From 6621e3d548f706c26b9083ba6bdb4e724282a07a Mon Sep 17 00:00:00 2001 From: August Johansson <148627186+webdevred@users.noreply.github.com> Date: Wed, 13 May 2026 20:00:54 +0200 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: August Johansson <148627186+webdevred@users.noreply.github.com> --- docs/_docs/integrations/defectdojo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/integrations/defectdojo.md b/docs/_docs/integrations/defectdojo.md index d29d3cf136..b0f9b43975 100644 --- a/docs/_docs/integrations/defectdojo.md +++ b/docs/_docs/integrations/defectdojo.md @@ -102,7 +102,7 @@ The DefectDojo documentation says 'If no test_title is provided, the latest test ![Configure Project](/images/screenshots/defectdojo_global_reimport.png) Alternatively, you can turn on the above reimport feature for all projects in one click, by checking on 'Enable reimport' box as shown in the screenshot above. -### Step 11: Add per project configuration for finding groups +#### Step 11: Add per project configuration for finding groups * Not supported in any release yet