Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/_docs/integrations/defectdojo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
webdevred marked this conversation as resolved.
Outdated

* 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` |
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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) {
Expand Down Expand Up @@ -164,7 +167,7 @@ public ArrayList<String> 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");
Expand All @@ -182,11 +185,13 @@ 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();
if(testTitle != null) {
.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));
}
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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<String> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

}
178 changes: 178 additions & 0 deletions src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
Expand Down
Loading