diff --git a/docs/_docs/integrations/defectdojo.md b/docs/_docs/integrations/defectdojo.md index ce8a130a01..bdf39e5715 100644 --- a/docs/_docs/integrations/defectdojo.md +++ b/docs/_docs/integrations/defectdojo.md @@ -101,3 +101,79 @@ 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. + +### Auto Context Creation (Optional) + +Instead of manually creating Products and Engagements in DefectDojo for each project, you can enable automatic context creation. When enabled, DefectDojo will automatically create the Product Type, Product, and Engagement if they don't already exist. + +#### Prerequisites +* Manual `defectdojo.engagementId` configuration always takes precedence over auto-create +* Auto-create is opt-in via global configuration + +#### Global Configuration + +Enable auto-create by setting the following configuration property: + +| Property Name | Default Value | Description | +|--------------|---------------|-------------| +| `defectdojo.autocreate.enabled` | `false` | Enable/disable auto context creation | +| `defectdojo.autocreate.engagementName` | `dependencytrack` | Default engagement name for all projects | +| `defectdojo.autocreate.productTypeName` | `Dependency Track` | Default product type name for all projects | +| `defectdojo.autocreate.deduplicationOnEngagement` | `false` | Enable deduplication at engagement level instead of product level | + +**About Deduplication:** +By default, DefectDojo deduplicates findings at the **Product level**, meaning duplicate findings are identified across all engagements within a product. Setting `deduplicationOnEngagement` to `true` changes this to deduplicate at the **Engagement level** instead, isolating duplicate detection within each engagement. + +#### Per-project Property Overrides (Optional) + +You can override the default names on a per-project basis: + +| Attribute | Value | +| ---------------| --------------------------------- | +| Group Name | `integrations` | +| Property Name | `defectdojo.autocreate.productName` | +| Property Value | Custom product name (defaults to project name if not set) | +| Property Type | `STRING` | + +| Attribute | Value | +| ---------------| --------------------------------- | +| Group Name | `integrations` | +| Property Name | `defectdojo.autocreate.engagementName` | +| Property Value | Custom engagement name (defaults to global config if not set) | +| Property Type | `STRING` | + +| Attribute | Value | +| ---------------| --------------------------------- | +| Group Name | `integrations` | +| Property Name | `defectdojo.autocreate.productTypeName` | +| Property Value | Custom product type name (defaults to global config if not set) | +| Property Type | `STRING` | + +| Attribute | Value | +| ---------------| --------------------------------- | +| Group Name | `integrations` | +| Property Name | `defectdojo.autocreate.deduplicationOnEngagement` | +| Property Value | `true` or `false` (defaults to global config if not set) | +| Property Type | `BOOLEAN` | + +#### Configuration Priority + +The configuration follows this priority order: + +1. **Manual `defectdojo.engagementId`** (highest priority) - Uses traditional flow with engagement ID +2. **Auto-create with per-project overrides** - Uses auto-create with project-specific names +3. **Auto-create with global defaults** - Uses auto-create with global configuration values + +#### Example: Using Auto-create + +1. Enable auto-create globally in Dependency-Track configuration: + - Set `defectdojo.autocreate.enabled` to `true` + - Optionally customize `defectdojo.autocreate.engagementName` and `defectdojo.autocreate.productTypeName` + +2. For each project, the system will automatically: + - Use the project name as the Product name (or override with `defectdojo.autocreate.productName`) + - Use the global engagement name (or override with `defectdojo.autocreate.engagementName`) + - Use the global product type name (or override with `defectdojo.autocreate.productTypeName`) + - Create these entities in DefectDojo if they don't exist + +3. No manual Product or Engagement creation in DefectDojo is required! diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 340eeb00b1..1eee1e7a1f 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -56,6 +56,11 @@ public DefectDojoClient(final DefectDojoUploader uploader, final URL baseURL) { } public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle) { + uploadDependencyTrackFindings(token, engagementId, findingsJson, verifyFindings, testTitle, null, null, null, false, false); + } + + public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle, + final String productTypeName, final String productName, final String engagementName, final boolean autoCreateContext, final boolean deduplicationOnEngagement) { 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"); @@ -64,7 +69,6 @@ public void uploadDependencyTrackFindings(final String token, final String engag MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE) .addPart("file", inputStreamBody) - .addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)) .addPart("scan_type", new StringBody("Dependency Track Finding Packaging Format (FPF) Export", ContentType.MULTIPART_FORM_DATA)) .addPart("verified", new StringBody(Boolean.toString(verifyFindings), ContentType.MULTIPART_FORM_DATA)) .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) @@ -72,6 +76,25 @@ 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 (autoCreateContext) { + // Use auto_create_context with product and engagement names + builder.addPart("auto_create_context", new StringBody("true", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("deduplication_on_engagement", new StringBody(Boolean.toString(deduplicationOnEngagement), ContentType.MULTIPART_FORM_DATA)); + if (productTypeName != null) { + builder.addPart("product_type_name", new StringBody(productTypeName, ContentType.MULTIPART_FORM_DATA)); + } + if (productName != null) { + builder.addPart("product_name", new StringBody(productName, ContentType.MULTIPART_FORM_DATA)); + } + if (engagementName != null) { + builder.addPart("engagement_name", new StringBody(engagementName, ContentType.MULTIPART_FORM_DATA)); + } + } else { + // Use traditional engagement ID + builder.addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)); + } + if(testTitle != null) { builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA)); } @@ -160,37 +183,60 @@ public ArrayList jsonToList(final JSONArray jsonArray) { return list; } + public void reimportDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final String testId, final Boolean doNotReactivate, final Boolean verifyFindings, final String testTitle) { + reimportDependencyTrackFindings(token, engagementId, findingsJson, testId, doNotReactivate, verifyFindings, testTitle, null, null, null, false, false); + } + /* * 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) { - LOGGER.debug("Re-reimport Dependency-Track findings to DefectDojo per Engagement"); + 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 productTypeName, final String productName, final String engagementName, final boolean autoCreateContext, final boolean deduplicationOnEngagement) { + LOGGER.debug("Reimporting Dependency-Track findings to DefectDojo"); HttpPost request = new HttpPost(baseURL + "/api/v2/reimport-scan/"); + InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json"); request.addHeader("accept", "application/json"); request.addHeader("Authorization", "Token " + token); - InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json"); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE) .addPart("file", inputStreamBody) - .addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)) .addPart("scan_type", new StringBody("Dependency Track Finding Packaging Format (FPF) Export", ContentType.MULTIPART_FORM_DATA)) .addPart("verified", new StringBody(Boolean.toString(verifyFindings), ContentType.MULTIPART_FORM_DATA)) .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) .addPart("minimum_severity", new StringBody("Info", ContentType.MULTIPART_FORM_DATA)) .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) .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(); - if(testTitle != null) { + .addPart("do_not_reactivate", new StringBody(Boolean.toString(doNotReactivate), ContentType.MULTIPART_FORM_DATA)); + + if (autoCreateContext) { + // Use auto_create_context with product and engagement names + builder.addPart("auto_create_context", new StringBody("true", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("deduplication_on_engagement", new StringBody(Boolean.toString(deduplicationOnEngagement), ContentType.MULTIPART_FORM_DATA)); + if (productTypeName != null) { + builder.addPart("product_type_name", new StringBody(productTypeName, ContentType.MULTIPART_FORM_DATA)); + } + if (productName != null) { + builder.addPart("product_name", new StringBody(productName, ContentType.MULTIPART_FORM_DATA)); + } + if (engagementName != null) { + builder.addPart("engagement_name", new StringBody(engagementName, ContentType.MULTIPART_FORM_DATA)); + } + } else { + // Use traditional engagement ID and test ID + builder.addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)); + builder.addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA)); + } + + if (testTitle != null) { builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA)); } + request.setEntity(builder.build()); try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { - LOGGER.debug("Successfully reimport findings to DefectDojo"); + LOGGER.debug("Successfully reimported findings to DefectDojo"); } else { uploader.handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); } diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index ab3a169287..00adf021f6 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -38,6 +38,10 @@ import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_URL; import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_REIMPORT_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT; public class DefectDojoUploader extends AbstractIntegrationPoint implements ProjectFindingUploader { @@ -47,6 +51,10 @@ 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 AUTOCREATE_PRODUCT_NAME_PROPERTY = "defectdojo.autocreate.productName"; + private static final String AUTOCREATE_ENGAGEMENT_NAME_PROPERTY = "defectdojo.autocreate.engagementName"; + private static final String AUTOCREATE_PRODUCT_TYPE_NAME_PROPERTY = "defectdojo.autocreate.productTypeName"; + private static final String AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT_PROPERTY = "defectdojo.autocreate.deduplicationOnEngagement"; public boolean isReimportConfigured(final Project project) { final ProjectProperty reimport = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), REIMPORT_PROPERTY); @@ -84,6 +92,55 @@ public String getTestTitle(final Project project) { return null; } + public boolean isAutoCreateEnabled() { + final ConfigProperty autoCreateEnabled = qm.getConfigProperty(DEFECTDOJO_AUTOCREATE_ENABLED.getGroupName(), DEFECTDOJO_AUTOCREATE_ENABLED.getPropertyName()); + return autoCreateEnabled != null && Boolean.parseBoolean(autoCreateEnabled.getPropertyValue()); + } + + public String getProductName(final Project project) { + final ProjectProperty productName = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), AUTOCREATE_PRODUCT_NAME_PROPERTY); + if (productName != null && productName.getPropertyValue() != null) { + return productName.getPropertyValue(); + } + return project.getName(); + } + + public String getEngagementName(final Project project) { + final ProjectProperty engagementName = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), AUTOCREATE_ENGAGEMENT_NAME_PROPERTY); + if (engagementName != null && engagementName.getPropertyValue() != null) { + return engagementName.getPropertyValue(); + } + final ConfigProperty globalEngagementName = qm.getConfigProperty(DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getGroupName(), DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getPropertyName()); + if (globalEngagementName != null && globalEngagementName.getPropertyValue() != null) { + return globalEngagementName.getPropertyValue(); + } + return "dependencytrack"; + } + + public String getProductTypeName(final Project project) { + final ProjectProperty productTypeName = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), AUTOCREATE_PRODUCT_TYPE_NAME_PROPERTY); + if (productTypeName != null && productTypeName.getPropertyValue() != null) { + return productTypeName.getPropertyValue(); + } + final ConfigProperty globalProductTypeName = qm.getConfigProperty(DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getGroupName(), DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getPropertyName()); + if (globalProductTypeName != null && globalProductTypeName.getPropertyValue() != null) { + return globalProductTypeName.getPropertyValue(); + } + return "Dependency Track"; + } + + public boolean isDeduplicationOnEngagementEnabled(final Project project) { + final ProjectProperty deduplicationOnEngagement = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT_PROPERTY); + if (deduplicationOnEngagement != null) { + return Boolean.parseBoolean(deduplicationOnEngagement.getPropertyValue()); + } + final ConfigProperty globalDeduplicationOnEngagement = qm.getConfigProperty(DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getGroupName(), DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getPropertyName()); + if (globalDeduplicationOnEngagement != null) { + return Boolean.parseBoolean(globalDeduplicationOnEngagement.getPropertyValue()); + } + return false; + } + @Override public String name() { return "DefectDojo"; @@ -103,7 +160,11 @@ public boolean isEnabled() { @Override public boolean isProjectConfigured(final Project project) { final ProjectProperty engagementId = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), ENGAGEMENTID_PROPERTY); - return engagementId != null && engagementId.getPropertyValue() != null; + if (engagementId != null && engagementId.getPropertyValue() != null) { + return true; + } + // If auto-create is enabled, project is considered configured + return isAutoCreateEnabled(); } @Override @@ -119,19 +180,73 @@ 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); + 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)); - LOGGER.debug("Found existing test Id: " + testId); - if (testId.equals("")) { - client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + + // Check if manual engagement ID is configured (takes precedence) + if (engagementId != null && engagementId.getPropertyValue() != null) { + // Traditional flow with engagement ID + if (isReimportConfigured(project) || globalReimportEnabled) { + final ArrayList testsIds = client.getDojoTestIds(apiKey.getPropertyValue(), engagementId.getPropertyValue()); + final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds, getTestTitle(project)); + LOGGER.debug("Found existing test Id: " + testId); + if (testId.equals("")) { + client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + } else { + client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, getTestTitle(project)); + } } else { - client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, getTestTitle(project)); + client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + } + } else if (isAutoCreateEnabled()) { + // Auto-create context flow + LOGGER.info("Using DefectDojo auto-create context for project: " + project.getName()); + final String productTypeName = getProductTypeName(project); + final String productName = getProductName(project); + final String engagementName = getEngagementName(project); + final boolean deduplicationOnEngagement = isDeduplicationOnEngagementEnabled(project); + + try { + if (isReimportConfigured(project) || globalReimportEnabled) { + // Use reimport-scan which automatically finds/creates/updates tests + LOGGER.debug("Reimport is enabled, using reimport-scan with auto-create context"); + client.reimportDependencyTrackFindings( + apiKey.getPropertyValue(), + null, // No engagement ID when using auto-create + payload, + null, // No test ID when using auto-create + isDoNotReactivateConfigured(project), + verifyFindings, + getTestTitle(project), + productTypeName, + productName, + engagementName, + true, // Enable auto-create context + deduplicationOnEngagement + ); + } else { + // Use import-scan for first-time creation + LOGGER.debug("Reimport is disabled, using import-scan with auto-create context"); + client.uploadDependencyTrackFindings( + apiKey.getPropertyValue(), + null, // No engagement ID when using auto-create + payload, + verifyFindings, + getTestTitle(project), + productTypeName, + productName, + engagementName, + true, // Enable auto-create context + deduplicationOnEngagement + ); + } + LOGGER.info("Successfully uploaded findings to DefectDojo with auto-created context"); + } catch (Exception e) { + LOGGER.warn("Failed to upload findings to DefectDojo with auto-create context", e); } } else { - client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project)); + LOGGER.warn("Project " + project.getName() + " has no engagement ID configured and auto-create is not enabled"); } } catch (Exception e) { LOGGER.error("An error occurred attempting to upload findings to DefectDojo", e); diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index b5fd00a2ac..ddb33b1e66 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -99,6 +99,10 @@ public enum ConfigPropertyConstants { DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo"), DEFECTDOJO_URL("integrations", "defectdojo.url", null, PropertyType.URL, "Base URL to DefectDojo"), DEFECTDOJO_API_KEY("integrations", "defectdojo.apiKey", null, PropertyType.STRING, "API Key for DefectDojo"), + DEFECTDOJO_AUTOCREATE_ENABLED("integrations", "defectdojo.autocreate.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo auto context creation"), + DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME("integrations", "defectdojo.autocreate.engagementName", "dependencytrack", PropertyType.STRING, "Default engagement name for DefectDojo auto context creation"), + DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME("integrations", "defectdojo.autocreate.productTypeName", "Dependency Track", PropertyType.STRING, "Default product type name for DefectDojo auto context creation"), + DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT("integrations", "defectdojo.autocreate.deduplicationOnEngagement", "false", PropertyType.BOOLEAN, "Flag to enable deduplication on engagement level instead of product level"), KENNA_ENABLED("integrations", "kenna.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable Kenna Security integration"), KENNA_API_URL("integrations", "kenna.api.url", "https://api.kennasecurity.com", PropertyType.STRING, "Kenna Security API URL"), KENNA_SYNC_CADENCE("integrations", "kenna.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Kenna Security"), diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java new file mode 100644 index 0000000000..92ec437468 --- /dev/null +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java @@ -0,0 +1,256 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.integrations.defectdojo; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.matching.EqualToPattern; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; + +@WireMockTest +class DefectDojoClientTest { + private WireMockRuntimeInfo wmRuntimeInfo; + + @BeforeEach + final void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + this.wmRuntimeInfo = wmRuntimeInfo; + } + + @Test + void testUploadWithAutoCreateAndDeduplicationEnabled() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.uploadDependencyTrackFindings( + "test-api-key", + null, + payload, + true, + "Test Title", + "Test Product Type", + "Test Product", + "Test Engagement", + true, + true + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("auto_create_context") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("deduplication_on_engagement") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("product_type_name") + .withBody(WireMock.equalTo("Test Product Type"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("product_name") + .withBody(WireMock.equalTo("Test Product"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("engagement_name") + .withBody(WireMock.equalTo("Test Engagement")))); + } + + @Test + void testUploadWithAutoCreateAndDeduplicationDisabled() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.uploadDependencyTrackFindings( + "test-api-key", + null, + payload, + true, + "Test Title", + "Test Product Type", + "Test Product", + "Test Engagement", + true, + false + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("auto_create_context") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("deduplication_on_engagement") + .withBody(WireMock.equalTo("false")))); + } + + @Test + void testUploadWithoutAutoCreate() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.uploadDependencyTrackFindings( + "test-api-key", + "12345", + payload, + true, + "Test Title" + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/import-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("engagement") + .withBody(WireMock.equalTo("12345")))); + } + + @Test + void testReimportWithAutoCreateAndDeduplicationEnabled() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.reimportDependencyTrackFindings( + "test-api-key", + null, + payload, + null, + false, + true, + "Test Title", + "Test Product Type", + "Test Product", + "Test Engagement", + true, + true + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("auto_create_context") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("deduplication_on_engagement") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("product_type_name") + .withBody(WireMock.equalTo("Test Product Type"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("product_name") + .withBody(WireMock.equalTo("Test Product"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("engagement_name") + .withBody(WireMock.equalTo("Test Engagement")))); + } + + @Test + void testReimportWithAutoCreateAndDeduplicationDisabled() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.reimportDependencyTrackFindings( + "test-api-key", + null, + payload, + null, + false, + true, + "Test Title", + "Test Product Type", + "Test Product", + "Test Engagement", + true, + false + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("auto_create_context") + .withBody(WireMock.equalTo("true"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("deduplication_on_engagement") + .withBody(WireMock.equalTo("false")))); + } + + @Test + void testReimportWithoutAutoCreate() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token test-api-key")) + .willReturn(WireMock.aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withStatus(201))); + + DefectDojoUploader uploader = new DefectDojoUploader(); + DefectDojoClient client = new DefectDojoClient(uploader, URI.create(wmRuntimeInfo.getHttpBaseUrl()).toURL()); + InputStream payload = new ByteArrayInputStream("test findings".getBytes()); + + client.reimportDependencyTrackFindings( + "test-api-key", + "12345", + payload, + "67890", + false, + true, + "Test Title" + ); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/api/v2/reimport-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("engagement") + .withBody(WireMock.equalTo("12345"))) + .withAnyRequestBodyPart(WireMock.aMultipart() + .withName("test") + .withBody(WireMock.equalTo("67890")))); + } +} diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index ace125478a..6000f728b6 100644 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -25,6 +25,10 @@ import org.junit.jupiter.api.Test; import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT; class DefectDojoUploaderTest extends PersistenceCapableTest { @@ -68,4 +72,226 @@ void testIntegrationDisabledCases() { Assertions.assertFalse(extension.isProjectConfigured(project)); } + @Test + void testAutoCreateEnabled() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_ENABLED.getGroupName(), + DEFECTDOJO_AUTOCREATE_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertTrue(extension.isAutoCreateEnabled()); + } + + @Test + void testAutoCreateDisabled() { + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertFalse(extension.isAutoCreateEnabled()); + } + + @Test + void testProjectConfiguredWithAutoCreate() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_ENABLED.getGroupName(), + DEFECTDOJO_AUTOCREATE_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertTrue(extension.isProjectConfigured(project)); + } + + @Test + void testGetProductNameDefault() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("ACME Example", extension.getProductName(project)); + } + + @Test + void testGetProductNameOverride() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.autocreate.productName", + "Custom Product Name", + IConfigProperty.PropertyType.STRING, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("Custom Product Name", extension.getProductName(project)); + } + + @Test + void testGetEngagementNameDefault() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("dependencytrack", extension.getEngagementName(project)); + } + + @Test + void testGetEngagementNameGlobalConfig() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getGroupName(), + DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getPropertyName(), + "global-engagement", + IConfigProperty.PropertyType.STRING, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("global-engagement", extension.getEngagementName(project)); + } + + @Test + void testGetEngagementNameProjectOverride() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getGroupName(), + DEFECTDOJO_AUTOCREATE_ENGAGEMENT_NAME.getPropertyName(), + "global-engagement", + IConfigProperty.PropertyType.STRING, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.autocreate.engagementName", + "project-engagement", + IConfigProperty.PropertyType.STRING, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("project-engagement", extension.getEngagementName(project)); + } + + @Test + void testGetProductTypeNameDefault() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("Dependency Track", extension.getProductTypeName(project)); + } + + @Test + void testGetProductTypeNameGlobalConfig() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getGroupName(), + DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getPropertyName(), + "Custom Product Type", + IConfigProperty.PropertyType.STRING, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("Custom Product Type", extension.getProductTypeName(project)); + } + + @Test + void testGetProductTypeNameProjectOverride() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getGroupName(), + DEFECTDOJO_AUTOCREATE_PRODUCT_TYPE_NAME.getPropertyName(), + "Global Product Type", + IConfigProperty.PropertyType.STRING, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.autocreate.productTypeName", + "Project Product Type", + IConfigProperty.PropertyType.STRING, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertEquals("Project Product Type", extension.getProductTypeName(project)); + } + + @Test + void testManualEngagementIdTakesPrecedence() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_ENABLED.getGroupName(), + DEFECTDOJO_AUTOCREATE_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.engagementId", + "12345", + IConfigProperty.PropertyType.STRING, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + // Project should be configured even with auto-create enabled + Assertions.assertTrue(extension.isProjectConfigured(project)); + } + + @Test + void testDeduplicationOnEngagementDefault() { + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertFalse(extension.isDeduplicationOnEngagementEnabled(project)); + } + + @Test + void testDeduplicationOnEngagementGlobalConfig() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getGroupName(), + DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertTrue(extension.isDeduplicationOnEngagementEnabled(project)); + } + + @Test + void testDeduplicationOnEngagementProjectOverride() { + qm.createConfigProperty( + DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getGroupName(), + DEFECTDOJO_AUTOCREATE_DEDUPLICATION_ON_ENGAGEMENT.getPropertyName(), + "false", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); + qm.createProjectProperty( + project, + DEFECTDOJO_ENABLED.getGroupName(), + "defectdojo.autocreate.deduplicationOnEngagement", + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + DefectDojoUploader extension = new DefectDojoUploader(); + extension.setQueryManager(qm); + Assertions.assertTrue(extension.isDeduplicationOnEngagementEnabled(project)); + } + }