diff --git a/docs/_docs/integrations/defectdojo.md b/docs/_docs/integrations/defectdojo.md index ce8a130a01..f611c5fdb6 100644 --- a/docs/_docs/integrations/defectdojo.md +++ b/docs/_docs/integrations/defectdojo.md @@ -14,6 +14,16 @@ Dependency-Track accomplishes this in the following ways: * Dependency-Track pushes findings to DefectDojo on a periodic basis (configurable) * DefectDojo parses Dependency-Track findings +### Forwarded analysis data + +Each finding pushed to DefectDojo includes audit data from Dependency-Track. The following fields influence how DefectDojo represents the finding: + +| Field | Description | +| ---------------------- | ----------- | +| `analysis.state` | A state of `FALSE_POSITIVE` marks the finding as a false positive in DefectDojo. Other states are included in the payload but are not acted on by the DefectDojo parser. | +| `analysis.isSuppressed`| Whether the finding is suppressed in Dependency-Track. | +| `analysis.detail` | Free-text analyst notes entered in Dependency-Track. Appended to the finding description in DefectDojo. Requires Dependency-Track v4.14.0 or higher and a compatible version of the DefectDojo Dependency Track parser. | + Requirements: * Dependency-Track v4.1.0 or higher * DefectDojo 1.13.1 or higher diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 400c615dc5..a49f7f21dc 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -96,6 +96,7 @@ public class Finding implements Serializable { , "FINDINGATTRIBUTION"."REFERENCE_URL" , "ANALYSIS"."STATE" , "ANALYSIS"."SUPPRESSED" + , "ANALYSIS"."DETAILS" FROM "COMPONENT" INNER JOIN "COMPONENTS_VULNERABILITIES" ON "COMPONENT"."ID" = "COMPONENTS_VULNERABILITIES"."COMPONENT_ID" @@ -151,6 +152,7 @@ public class Finding implements Serializable { , "FINDINGATTRIBUTION"."REFERENCE_URL" , "ANALYSIS"."STATE" , "ANALYSIS"."SUPPRESSED" + , "ANALYSIS"."DETAILS" , "PROJECT"."UUID" , "PROJECT"."NAME" , "PROJECT"."VERSION" @@ -245,10 +247,15 @@ public Finding(UUID project, Object... o) { optValue(analysis, "state", o[34]); optValue(analysis, "isSuppressed", o[35], false); + if (o[36] instanceof final Clob clob) { + optValue(analysis, "detail", toString(clob)); + } else { + optValue(analysis, "detail", o[36]); + } - if (o.length > 36) { - optValue(component, "projectName", o[37]); - optValue(component, "projectVersion", o[38]); + if (o.length > 37) { + optValue(component, "projectName", o[38]); + optValue(component, "projectVersion", o[39]); } } diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index a91de4dbaa..9406bbf10a 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -137,7 +137,7 @@ public PaginatedResult getAllFindings(final Map filters, final b final List list = totalList.subList(this.pagination.getOffset(), Math.min(this.pagination.getOffset() + this.pagination.getLimit(), totalList.size())); final List findings = new ArrayList<>(); for (final Object[] o : list) { - final Finding finding = new Finding(UUID.fromString((String) o[36]), o); + final Finding finding = new Finding(UUID.fromString((String) o[37]), o); final Component component = getObjectByUuid(Component.class, (String) finding.getComponent().get("uuid")); final Vulnerability vulnerability = getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid")); final Analysis analysis = getAnalysis(component, vulnerability); diff --git a/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java b/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java index d610ae6487..595f200f38 100644 --- a/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java +++ b/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java @@ -92,7 +92,8 @@ void testFindingsVulnerabilityAndAliases() { null, // 32 null, // 33 AnalysisState.NOT_AFFECTED, // 34 - true // 35 + true, // 35 + null // 36 ); Finding findingWithAlias = new Finding(project.getUuid(), "component-uuid-2", "component-name-2", "component-group", @@ -119,7 +120,8 @@ void testFindingsVulnerabilityAndAliases() { null, // 32 null, // 33 AnalysisState.NOT_AFFECTED, // 34 - true // 35 + true, // 35 + null // 36 ); var alias = new VulnerabilityAlias(); diff --git a/src/test/java/org/dependencytrack/model/FindingTest.java b/src/test/java/org/dependencytrack/model/FindingTest.java index 3c7f09f652..7d1a6fbac4 100644 --- a/src/test/java/org/dependencytrack/model/FindingTest.java +++ b/src/test/java/org/dependencytrack/model/FindingTest.java @@ -58,7 +58,8 @@ class FindingTest extends PersistenceCapableTest { null, // 32 null, // 33 AnalysisState.NOT_AFFECTED, // 34 - true // 35 + true, // 35 + "analysis-detail" // 36 ); @Test @@ -102,6 +103,7 @@ void testAnalysis() { Map map = finding.getAnalysis(); Assertions.assertEquals(AnalysisState.NOT_AFFECTED, map.get("state")); Assertions.assertEquals(true, map.get("isSuppressed")); + Assertions.assertEquals("analysis-detail", map.get("detail")); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index f6f58a9b16..1ce28e73d6 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -31,6 +31,7 @@ import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestExtension; import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; @@ -419,6 +420,24 @@ void getAllFindings() { Assertions.assertEquals(p2.getUuid().toString(), json.getJsonObject(4).getJsonObject("component").getString("project")); } + @Test + void getAllFindingsIncludesAnalysisDetail() { + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component component = createComponent(project, "Component A", "1.0"); + Vulnerability vuln = createVulnerability("Vuln-1", Severity.HIGH); + qm.addVulnerability(vuln, component, AnalyzerIdentity.NONE); + qm.makeAnalysis(component, vuln, AnalysisState.NOT_AFFECTED, null, null, "audit detail text", false); + + Response response = jersey.target(V1_FINDING).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Assertions.assertEquals(200, response.getStatus()); + JsonArray json = parseJsonArray(response); + Assertions.assertEquals(1, json.size()); + Assertions.assertEquals("audit detail text", json.getJsonObject(0).getJsonObject("analysis").getString("detail")); + } + @Test void getAllFindingsWithAclEnabled() { Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false);