Skip to content

Commit fadd195

Browse files
webdevredCopilot
andauthored
Send group_by to defect dojo (#6130)
* test(integrations): add tests for defectdojo.groupBy per-project property Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> * 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> * fix(integrations): remove spurious build() call in reimportDependencyTrackFindings Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> * Updated the DefectDojo integration documentation Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> * 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> --------- Signed-off-by: webdevred <148627186+webdevred@users.noreply.github.com> Signed-off-by: August Johansson <148627186+webdevred@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ff16a36 commit fadd195

5 files changed

Lines changed: 250 additions & 10 deletions

File tree

docs/_docs/integrations/defectdojo.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,25 @@ The DefectDojo documentation says 'If no test_title is provided, the latest test
101101
* Dependency-Track v4.6.0 or higher
102102
![Configure Project](/images/screenshots/defectdojo_global_reimport.png)
103103
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.
104+
105+
#### Step 11: Add per project configuration for finding groups
106+
107+
* Not supported in any release yet
108+
109+
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.
110+
111+
If this property is not set, no grouping is applied and DefectDojo's default behavior is used.
112+
113+
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:
114+
115+
* `component_name`
116+
* `component_name+component_version`
117+
* `file_path`
118+
* `finding_title`
119+
120+
| Attribute | Value |
121+
|----------------|---------------------------------------------------------------|
122+
| Group Name | `integrations` |
123+
| Property Name | `defectdojo.groupBy` |
124+
| Property Value | One of the supported `group_by` values, e.g. `component_name` |
125+
| Property Type | `STRING` |

src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public DefectDojoClient(final DefectDojoUploader uploader, final URL baseURL) {
5555
this.baseURL = baseURL;
5656
}
5757

58-
public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle) {
58+
public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final Boolean verifyFindings, final String testTitle, final String groupBy) {
5959
LOGGER.debug("Uploading Dependency-Track findings to DefectDojo");
6060
HttpPost request = new HttpPost(baseURL + "/api/v2/import-scan/");
6161
InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json");
@@ -72,9 +72,12 @@ public void uploadDependencyTrackFindings(final String token, final String engag
7272
.addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA))
7373
.addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA))
7474
.addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA));
75-
if(testTitle != null) {
75+
if (testTitle != null) {
7676
builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA));
7777
}
78+
if (groupBy != null) {
79+
builder.addPart("group_by", new StringBody(groupBy, ContentType.MULTIPART_FORM_DATA));
80+
}
7881
request.setEntity(builder.build());
7982
try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) {
8083
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {
@@ -164,7 +167,7 @@ public ArrayList<String> jsonToList(final JSONArray jsonArray) {
164167
* A Reimport will reuse (overwrite) the existing test, instead of create a new test.
165168
* The Successfully reimport will also increase the reimport counter by 1.
166169
*/
167-
public void reimportDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final String testId, final Boolean doNotReactivate, final Boolean verifyFindings, final String testTitle) {
170+
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) {
168171
LOGGER.debug("Re-reimport Dependency-Track findings to DefectDojo per Engagement");
169172
HttpPost request = new HttpPost(baseURL + "/api/v2/reimport-scan/");
170173
request.addHeader("accept", "application/json");
@@ -182,11 +185,13 @@ public void reimportDependencyTrackFindings(final String token, final String eng
182185
.addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA))
183186
.addPart("do_not_reactivate", new StringBody(doNotReactivate.toString(), ContentType.MULTIPART_FORM_DATA))
184187
.addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA))
185-
.addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA))
186-
.build();
187-
if(testTitle != null) {
188+
.addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA));
189+
if (testTitle != null) {
188190
builder.addPart("test_title", new StringBody(testTitle, ContentType.MULTIPART_FORM_DATA));
189191
}
192+
if (groupBy != null) {
193+
builder.addPart("group_by", new StringBody(groupBy, ContentType.MULTIPART_FORM_DATA));
194+
}
190195
request.setEntity(builder.build());
191196
try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) {
192197
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {

src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class DefectDojoUploader extends AbstractIntegrationPoint implements Proj
4747
private static final String DO_NOT_REACTIVATE_PROPERTY = "defectdojo.doNotReactivate";
4848
private static final String VERIFIED_PROPERTY = "defectdojo.verified";
4949
private static final String TEST_TITLE_PROPERTY = "defectdojo.testTitle";
50+
private static final String GROUP_BY_PROPERTY = "defectdojo.groupBy";
5051

5152
public boolean isReimportConfigured(final Project project) {
5253
final ProjectProperty reimport = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), REIMPORT_PROPERTY);
@@ -84,6 +85,14 @@ public String getTestTitle(final Project project) {
8485
return null;
8586
}
8687

88+
public String getGroupBy(final Project project) {
89+
final ProjectProperty groupBy = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), GROUP_BY_PROPERTY);
90+
if (groupBy != null && groupBy.getPropertyValue() != null) {
91+
return groupBy.getPropertyValue();
92+
}
93+
return null;
94+
}
95+
8796
@Override
8897
public String name() {
8998
return "DefectDojo";
@@ -119,19 +128,21 @@ public void upload(final Project project, final InputStream payload) {
119128
final boolean globalReimportEnabled = qm.isEnabled(DEFECTDOJO_REIMPORT_ENABLED);
120129
final ProjectProperty engagementId = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), ENGAGEMENTID_PROPERTY);
121130
final boolean verifyFindings = isVerifiedConfigured(project);
131+
final String testTitle = getTestTitle(project);
132+
final String groupBy = getGroupBy(project);
122133
try {
123134
final DefectDojoClient client = new DefectDojoClient(this, URI.create(defectDojoUrl.getPropertyValue()).toURL());
124135
if (isReimportConfigured(project) || globalReimportEnabled) {
125136
final ArrayList<String> testsIds = client.getDojoTestIds(apiKey.getPropertyValue(), engagementId.getPropertyValue());
126-
final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds, getTestTitle(project));
137+
final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds, testTitle);
127138
LOGGER.debug("Found existing test Id: " + testId);
128139
if (testId.equals("")) {
129-
client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project));
140+
client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, testTitle, groupBy);
130141
} else {
131-
client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, getTestTitle(project));
142+
client.reimportDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, testId, isDoNotReactivateConfigured(project), verifyFindings, testTitle, groupBy);
132143
}
133144
} else {
134-
client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, getTestTitle(project));
145+
client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload, verifyFindings, testTitle, groupBy);
135146
}
136147
} catch (Exception e) {
137148
LOGGER.error("An error occurred attempting to upload findings to DefectDojo", e);

src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,28 @@ void testIntegrationDisabledCases() {
6868
Assertions.assertFalse(extension.isProjectConfigured(project));
6969
}
7070

71+
@Test
72+
void testGetGroupByReturnsNullWhenNotConfigured() {
73+
Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false);
74+
DefectDojoUploader extension = new DefectDojoUploader();
75+
extension.setQueryManager(qm);
76+
Assertions.assertNull(extension.getGroupBy(project));
77+
}
78+
79+
@Test
80+
void testGetGroupByReturnsValueWhenConfigured() {
81+
Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false);
82+
qm.createProjectProperty(
83+
project,
84+
DEFECTDOJO_ENABLED.getGroupName(),
85+
"defectdojo.groupBy",
86+
"component_name",
87+
IConfigProperty.PropertyType.STRING,
88+
null
89+
);
90+
DefectDojoUploader extension = new DefectDojoUploader();
91+
extension.setQueryManager(qm);
92+
Assertions.assertEquals("component_name", extension.getGroupBy(project));
93+
}
94+
7195
}

src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,184 @@ void testUploadWithReimportAndNoExistingTest() {
909909
""", true, false))));
910910
}
911911

912+
@Test
913+
void testUploadWithGroupBy() {
914+
qm.createConfigProperty(
915+
DEFECTDOJO_ENABLED.getGroupName(),
916+
DEFECTDOJO_ENABLED.getPropertyName(),
917+
"true",
918+
DEFECTDOJO_ENABLED.getPropertyType(),
919+
null
920+
);
921+
qm.createConfigProperty(
922+
DEFECTDOJO_URL.getGroupName(),
923+
DEFECTDOJO_URL.getPropertyName(),
924+
wmRuntimeInfo.getHttpBaseUrl(),
925+
DEFECTDOJO_URL.getPropertyType(),
926+
null
927+
);
928+
qm.createConfigProperty(
929+
DEFECTDOJO_API_KEY.getGroupName(),
930+
DEFECTDOJO_API_KEY.getPropertyName(),
931+
"dojoApiKey",
932+
DEFECTDOJO_API_KEY.getPropertyType(),
933+
null
934+
);
935+
qm.createConfigProperty(
936+
DEFECTDOJO_REIMPORT_ENABLED.getGroupName(),
937+
DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(),
938+
DEFECTDOJO_REIMPORT_ENABLED.getDefaultPropertyValue(),
939+
DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(),
940+
null
941+
);
942+
943+
stubFor(post(urlPathEqualTo("/api/v2/import-scan/"))
944+
.willReturn(aResponse()
945+
.withStatus(201)));
946+
947+
final var project = new Project();
948+
project.setName("acme-app");
949+
project.setVersion("1.0.0");
950+
qm.persist(project);
951+
952+
final var component = new Component();
953+
component.setProject(project);
954+
component.setName("acme-lib");
955+
component.setVersion("1.2.3");
956+
qm.persist(component);
957+
958+
qm.createProjectProperty(project, "integrations", "defectdojo.engagementId",
959+
"666", IConfigProperty.PropertyType.STRING, null);
960+
qm.createProjectProperty(project, "integrations", "defectdojo.groupBy",
961+
"component_name", IConfigProperty.PropertyType.STRING, null);
962+
963+
new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract());
964+
965+
verify(postRequestedFor(urlPathEqualTo("/api/v2/import-scan/"))
966+
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey"))
967+
.withAnyRequestBodyPart(aMultipart()
968+
.withName("engagement")
969+
.withBody(equalTo("666")))
970+
.withAnyRequestBodyPart(aMultipart()
971+
.withName("group_by")
972+
.withBody(equalTo("component_name"))));
973+
}
974+
975+
@Test
976+
void testUploadWithReimportAndGroupBy() {
977+
qm.createConfigProperty(
978+
DEFECTDOJO_ENABLED.getGroupName(),
979+
DEFECTDOJO_ENABLED.getPropertyName(),
980+
"true",
981+
DEFECTDOJO_ENABLED.getPropertyType(),
982+
null
983+
);
984+
qm.createConfigProperty(
985+
DEFECTDOJO_URL.getGroupName(),
986+
DEFECTDOJO_URL.getPropertyName(),
987+
wmRuntimeInfo.getHttpBaseUrl(),
988+
DEFECTDOJO_URL.getPropertyType(),
989+
null
990+
);
991+
qm.createConfigProperty(
992+
DEFECTDOJO_API_KEY.getGroupName(),
993+
DEFECTDOJO_API_KEY.getPropertyName(),
994+
"dojoApiKey",
995+
DEFECTDOJO_API_KEY.getPropertyType(),
996+
null
997+
);
998+
qm.createConfigProperty(
999+
DEFECTDOJO_REIMPORT_ENABLED.getGroupName(),
1000+
DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(),
1001+
"false",
1002+
DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(),
1003+
null
1004+
);
1005+
1006+
stubFor(get(urlPathEqualTo("/api/v2/tests/"))
1007+
.withQueryParam("engagement", equalTo("666"))
1008+
.withQueryParam("limit", equalTo("100"))
1009+
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey"))
1010+
.willReturn(aResponse()
1011+
.withStatus(200)
1012+
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
1013+
.withBody("""
1014+
{
1015+
"count": 1,
1016+
"next": null,
1017+
"previous": null,
1018+
"results": [
1019+
{
1020+
"id": 1,
1021+
"tags": [],
1022+
"test_type_name": "Dependency Track Finding Packaging Format (FPF) Export",
1023+
"finding_groups": [],
1024+
"scan_type": "Dependency Track Finding Packaging Format (FPF) Export",
1025+
"title": null,
1026+
"description": null,
1027+
"target_start": "2023-04-29T00:00:00Z",
1028+
"target_end": "2023-04-29T21:39:21.513481Z",
1029+
"estimated_time": null,
1030+
"actual_time": null,
1031+
"percent_complete": 100,
1032+
"updated": "2023-04-29T21:39:21.617857Z",
1033+
"created": "2023-04-29T21:39:21.516216Z",
1034+
"version": "",
1035+
"build_id": "",
1036+
"commit_hash": "",
1037+
"branch_tag": "",
1038+
"engagement": 666,
1039+
"lead": 1,
1040+
"test_type": 63,
1041+
"environment": 7,
1042+
"api_scan_configuration": null,
1043+
"notes": [],
1044+
"files": []
1045+
}
1046+
],
1047+
"prefetch": {}
1048+
}
1049+
""")));
1050+
1051+
stubFor(post(urlPathEqualTo("/api/v2/reimport-scan/"))
1052+
.willReturn(aResponse()
1053+
.withStatus(201)));
1054+
1055+
final var project = new Project();
1056+
project.setName("acme-app");
1057+
project.setVersion("1.0.0");
1058+
qm.persist(project);
1059+
1060+
final var component = new Component();
1061+
component.setProject(project);
1062+
component.setName("acme-lib");
1063+
component.setVersion("1.2.3");
1064+
qm.persist(component);
1065+
1066+
qm.createProjectProperty(project, "integrations", "defectdojo.engagementId",
1067+
"666", IConfigProperty.PropertyType.STRING, null);
1068+
qm.createProjectProperty(project, "integrations", "defectdojo.reimport",
1069+
"true", IConfigProperty.PropertyType.BOOLEAN, null);
1070+
qm.createProjectProperty(project, "integrations", "defectdojo.groupBy",
1071+
"component_name+component_version", IConfigProperty.PropertyType.STRING, null);
1072+
1073+
new DefectDojoUploadTask().inform(new DefectDojoUploadEventAbstract());
1074+
1075+
verify(1, getRequestedFor(urlPathEqualTo("/api/v2/tests/")));
1076+
1077+
verify(postRequestedFor(urlPathEqualTo("/api/v2/reimport-scan/"))
1078+
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey"))
1079+
.withAnyRequestBodyPart(aMultipart()
1080+
.withName("engagement")
1081+
.withBody(equalTo("666")))
1082+
.withAnyRequestBodyPart(aMultipart()
1083+
.withName("test")
1084+
.withBody(equalTo("1")))
1085+
.withAnyRequestBodyPart(aMultipart()
1086+
.withName("group_by")
1087+
.withBody(equalTo("component_name+component_version"))));
1088+
}
1089+
9121090
/**
9131091
* Un-ignore this test to test the integration against a local DefectDojo deployment.
9141092
* <p>

0 commit comments

Comments
 (0)