diff --git a/docs/_docs/integrations/defectdojo.md b/docs/_docs/integrations/defectdojo.md index b0f9b43975..037b39a417 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/docs/_docs/integrations/file-formats.md b/docs/_docs/integrations/file-formats.md index 9f989ab093..1aabff104a 100644 --- a/docs/_docs/integrations/file-formats.md +++ b/docs/_docs/integrations/file-formats.md @@ -39,11 +39,13 @@ The **VIEW_VULNERABILITY** permission is required to use the findings API. > It adds optional `cvssV2Vector`, `cvssV3Vector`, `cvssV4Vector`, and `owaspRRVector` fields to `vulnerability` objects, > which are included when the corresponding scores or ratings are available and may be omitted otherwise. +> Finding Packaging Format v1.4 adds an optional `detail` field to `analysis` objects, which contains any analyst notes recorded against the finding. + #### Example ```json { - "version": "1.3", + "version": "1.4", "meta" : { "application": "Dependency-Track", "version": "4.5.0", @@ -89,7 +91,8 @@ The **VIEW_VULNERABILITY** permission is required to use the findings API. }, "analysis": { "state": "NOT_SET", - "isSuppressed": false + "isSuppressed": false, + "detail": "Reviewed and confirmed not exploitable in this context." }, "matrix": "ca4f2da9-0fad-4a13-92d7-f627f3168a56:b815b581-fec1-4374-a871-68862a8f8d52:115b80bb-46c4-41d1-9f10-8a175d4abb46" }, diff --git a/src/main/java/org/dependencytrack/integrations/FindingPackagingFormat.java b/src/main/java/org/dependencytrack/integrations/FindingPackagingFormat.java index b52eb77ee2..d93547f0d5 100644 --- a/src/main/java/org/dependencytrack/integrations/FindingPackagingFormat.java +++ b/src/main/java/org/dependencytrack/integrations/FindingPackagingFormat.java @@ -35,7 +35,7 @@ public class FindingPackagingFormat { /** FPF is versioned. If the format changes, the version needs to be bumped. */ - private static final String FPF_VERSION = "1.3"; + private static final String FPF_VERSION = "1.4"; private static final String FIELD_APPLICATION = "application"; private static final String FIELD_VERSION = "version"; private static final String FIELD_TIMESTAMP = "timestamp"; 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..b8d68422dd 100644 --- a/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java +++ b/src/test/java/org/dependencytrack/integrations/FindingPackagingFormatTest.java @@ -60,7 +60,7 @@ void wrapperTest() { Assertions.assertEquals(project.getDescription(), pjson.getString("description")); Assertions.assertEquals(project.getVersion(), pjson.getString("version")); - Assertions.assertEquals("1.3", root.getString("version")); + Assertions.assertEquals("1.4", root.getString("version")); } @Test @@ -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..b09352b5da 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; @@ -193,7 +194,7 @@ void exportFindingsByProjectTest() { Assertions.assertEquals("Acme Example", json.getJsonObject("project").getString("name")); Assertions.assertEquals("1.0", json.getJsonObject("project").getString("version")); Assertions.assertEquals(p1.getUuid().toString(), json.getJsonObject("project").getString("uuid")); - Assertions.assertEquals("1.3", json.getString("version")); // FPF version + Assertions.assertEquals("1.4", json.getString("version")); // FPF version JsonArray findings = json.getJsonArray("findings"); Assertions.assertEquals(3, findings.size()); Assertions.assertEquals("Component A", findings.getJsonObject(0).getJsonObject("component").getString("name")); @@ -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); diff --git a/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java b/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java index 021041568d..5a5c365a7c 100644 --- a/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/DefectDojoUploadTaskTest.java @@ -147,7 +147,7 @@ void testUpload() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}", @@ -279,7 +279,7 @@ void testUploadWithTestName() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}", @@ -527,7 +527,7 @@ void testUploadWithGlobalReimport() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}", @@ -674,7 +674,7 @@ void testUploadWithProjectLevelReimport() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}", @@ -801,7 +801,7 @@ void testUploadWithProjectLevelReimportAndTestName() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}", @@ -893,7 +893,7 @@ void testUploadWithReimportAndNoExistingTest() { .withName("file") .withBody(equalToJson(""" { - "version": "1.3", + "version": "1.4", "meta": { "application": "Dependency-Track", "version": "${json-unit.any-string}",