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..97398ee637 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; } + @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(); + } + return null; + } + @Override public String name() { return "DefectDojo"; @@ -142,13 +151,14 @@ 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) { 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()); @@ -160,7 +170,8 @@ public void upload(final Project project, final InputStream payload) { engagementId.getPropertyValue(), payload, verifyFindings, - testTitle); + testTitle, + groupBy); } else { client.reimportDependencyTrackFindings( apiKeyValue, @@ -169,7 +180,8 @@ public void upload(final Project project, final InputStream payload) { testId, isDoNotReactivateConfigured(project), verifyFindings, - testTitle); + testTitle, + groupBy); } } else { client.uploadDependencyTrackFindings( @@ -177,7 +189,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); 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..9ecad72003 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, null, 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, null, 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..bd30be8e08 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java @@ -931,6 +931,190 @@ void testUploadWithReimportAndNoExistingTest(WireMockRuntimeInfo wmRuntimeInfo) """, true, false)))); } + @Test + void testUploadWithGroupBy(WireMockRuntimeInfo wmRuntimeInfo) { + 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(), + "apiKeySecretName", + 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( + HttpClient.newHttpClient(), + new TestSecretManager(Map.of("apiKeySecretName", "dojoApiKey"))) + .run(); + + 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(WireMockRuntimeInfo wmRuntimeInfo) { + 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(), + "apiKeySecretName", + 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( + HttpClient.newHttpClient(), + new TestSecretManager(Map.of("apiKeySecretName", "dojoApiKey"))) + .run(); + + 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. *