From 7d9eb7efc725b88a6f746eb989b3a84af625bbd3 Mon Sep 17 00:00:00 2001 From: omobolaji adeyan Date: Mon, 22 Jun 2026 00:12:34 -0500 Subject: [PATCH 1/3] Document finding response schema Define the nested fields returned by the v1 finding endpoints so generated OpenAPI clients receive typed component, vulnerability, analysis, and attribution models instead of free-form objects. Add a database-free regression test for the generated contract. Signed-off-by: omobolaji adeyan --- .../resources/v1/FindingResource.java | 5 +- .../resources/v1/vo/FindingResponse.java | 125 ++++++++++++++++++ .../v1/FindingOpenApiSchemaTest.java | 72 ++++++++++ 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java create mode 100644 apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 7852d3d9b6..135c315054 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -66,6 +66,7 @@ import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.resources.v1.vo.BomUploadResponse; +import org.dependencytrack.resources.v1.vo.FindingResponse; import org.dependencytrack.util.PersistenceUtil; import org.dependencytrack.util.PurlUtil; import org.slf4j.Logger; @@ -125,7 +126,7 @@ public class FindingResource extends AbstractApiResource { description = "A list of all findings for a specific project, or a SARIF file", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of findings", schema = @Schema(format = "integer")), content = { - @Content(array = @ArraySchema(schema = @Schema(implementation = Finding.class)), mediaType = MediaType.APPLICATION_JSON), + @Content(array = @ArraySchema(schema = @Schema(implementation = FindingResponse.class)), mediaType = MediaType.APPLICATION_JSON), @Content(schema = @Schema(type = "string"), mediaType = MEDIA_TYPE_SARIF_JSON) } ), @@ -302,7 +303,7 @@ public Response analyzeProject( responseCode = "200", description = "A list of all findings", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of findings", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Finding.class))) + content = @Content(array = @ArraySchema(schema = @Schema(implementation = FindingResponse.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), }) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java new file mode 100644 index 0000000000..c41e81a5c5 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java @@ -0,0 +1,125 @@ +/* + * 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.resources.v1.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.Scope; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability.Source; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/// OpenAPI representation of a finding returned by the v1 finding endpoints. +/// +/// @since 5.1.0 +@NullMarked +@Schema(name = "Finding", description = "A vulnerability finding for a project component") +public record FindingResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Component component, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) VulnerabilityDetails vulnerability, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Analysis analysis, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Attribution attribution, + @Schema(description = "Composite project, component, and vulnerability identifier", requiredMode = Schema.RequiredMode.REQUIRED) + String matrix) { + + @Schema(name = "FindingComponent") + public record Component( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID uuid, + @Nullable String name, + @Nullable String group, + @Nullable String version, + @Nullable String purl, + @Nullable String cpe, + @Schema(description = "UUID of the project containing the component", requiredMode = Schema.RequiredMode.REQUIRED) + UUID project, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean hasOccurrences, + @Nullable Scope scope, + @Nullable String projectName, + @Nullable String projectVersion) { + } + + @Schema(name = "FindingVulnerability") + public record VulnerabilityDetails( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID uuid, + @Nullable Source source, + @Nullable String vulnId, + @Nullable String title, + @Nullable String subtitle, + @Nullable String description, + @Nullable String recommendation, + @Nullable String references, + @Nullable Severity severity, + @Nullable Integer severityRank, + @Nullable BigDecimal cvssV2BaseScore, + @Nullable BigDecimal cvssV3BaseScore, + @Nullable BigDecimal cvssV4Score, + @Nullable String cvssV2Vector, + @Nullable String cvssV3Vector, + @Nullable String cvssV4Vector, + @Nullable BigDecimal owaspLikelihoodScore, + @Nullable BigDecimal owaspTechnicalImpactScore, + @Nullable BigDecimal owaspBusinessImpactScore, + @Nullable String owaspRRVector, + @Nullable BigDecimal epssScore, + @Nullable BigDecimal epssPercentile, + @Nullable List cwes, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Set aliases, + @Schema(type = "integer", format = "int64", description = "Publication timestamp in milliseconds since the Unix epoch") + @Nullable Long published) { + } + + @Schema(name = "FindingCwe") + public record Cwe( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int cweId, + @Nullable String name) { + } + + @Schema(name = "FindingVulnerabilityAlias") + public record Alias( + @Nullable String cveId, + @Nullable String ghsaId, + @Nullable String sonatypeId, + @Nullable String osvId, + @Nullable String snykId, + @Nullable String vulnDbId) { + } + + @Schema(name = "FindingAnalysis") + public record Analysis( + @Nullable AnalysisState state, + @JsonProperty("isSuppressed") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean isSuppressed) { + } + + @Schema(name = "FindingAttribution") + public record Attribution( + @Nullable String analyzerIdentity, + @Schema(type = "integer", format = "int64", description = "Attribution timestamp in milliseconds since the Unix epoch") + @Nullable Long attributedOn, + @Nullable String alternateIdentifier, + @Nullable String referenceUrl) { + } +} diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java new file mode 100644 index 0000000000..cd242477e0 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java @@ -0,0 +1,72 @@ +/* + * 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.resources.v1; + +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.media.Schema; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +class FindingOpenApiSchemaTest { + + @Test + void shouldDescribeFindingResponse() throws IOException { + final String spec; + try (final var inputStream = FindingOpenApiSchemaTest.class + .getResourceAsStream("/org/dependencytrack/api/v1/openapi.yaml")) { + spec = new String(requireNonNull(inputStream).readAllBytes(), StandardCharsets.UTF_8); + } + + final var parseResult = new OpenAPIParser().readContents(spec, null, null); + assertThat(parseResult.getMessages()).isEmpty(); + + final var openApi = parseResult.getOpenAPI(); + final Schema responseSchema = openApi.getPaths() + .get("/v1/finding/project/{uuid}") + .getGet() + .getResponses() + .get("200") + .getContent() + .get("application/json") + .getSchema(); + assertThat(responseSchema.getItems().get$ref()).isEqualTo("#/components/schemas/Finding"); + + final var schemas = openApi.getComponents().getSchemas(); + assertThat(propertyNames(schemas.get("Finding"))) + .contains("component", "vulnerability", "analysis", "attribution", "matrix"); + assertThat(propertyNames(schemas.get("FindingComponent"))) + .contains("uuid", "name", "version", "project", "hasOccurrences"); + assertThat(propertyNames(schemas.get("FindingVulnerability"))) + .contains("uuid", "source", "vulnId", "severity", "cwes", "aliases"); + assertThat(propertyNames(schemas.get("FindingAnalysis"))) + .contains("state", "isSuppressed"); + assertThat(propertyNames(schemas.get("FindingAttribution"))) + .contains("analyzerIdentity", "attributedOn", "alternateIdentifier", "referenceUrl"); + } + + private static Set propertyNames(final Schema schema) { + return schema.getProperties().keySet(); + } +} From d7709b15777ce7d8a1eaa9101da5a030d43d9c01 Mon Sep 17 00:00:00 2001 From: omobolaji adeyan Date: Mon, 22 Jun 2026 13:52:53 -0500 Subject: [PATCH 2/3] Address finding response review feedback Signed-off-by: omobolaji adeyan --- .../resources/v1/FindingResource.java | 8 +- .../resources/v1/vo/FindingResponse.java | 104 ++++++++++++++++++ .../v1/FindingOpenApiSchemaTest.java | 72 ------------ 3 files changed, 110 insertions(+), 74 deletions(-) delete mode 100644 apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 135c315054..77b1baff7a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -196,7 +196,9 @@ public Response getFindingsByProject(@Parameter(description = "The UUID of the p return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("An error occurred while generating SARIF file").build(); } } - return Response.ok(findings).header(TOTAL_COUNT_HEADER, totalCount).build(); + return Response.ok(findings.stream().map(FindingResponse::of).toList()) + .header(TOTAL_COUNT_HEADER, totalCount) + .build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -376,7 +378,9 @@ public Response getAllFindings(@Parameter(description = "Show inactive projects" final long totalCount = findingRows.isEmpty() ? 0 : findingRows.getFirst().totalCount(); List findings = findingRows.stream().map(Finding::new).toList(); findings = mapComponentLatestVersion(findings); - return Response.ok(findings).header(TOTAL_COUNT_HEADER, totalCount).build(); + return Response.ok(findings.stream().map(FindingResponse::of).toList()) + .header(TOTAL_COUNT_HEADER, totalCount) + .build(); } @GET diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java index c41e81a5c5..7088a442b9 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.Finding; import org.dependencytrack.model.Scope; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability.Source; @@ -28,7 +29,9 @@ import org.jspecify.annotations.Nullable; import java.math.BigDecimal; +import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -45,6 +48,107 @@ public record FindingResponse( @Schema(description = "Composite project, component, and vulnerability identifier", requiredMode = Schema.RequiredMode.REQUIRED) String matrix) { + public static FindingResponse of(final Finding finding) { + final Map component = finding.getComponent(); + final Map vulnerability = finding.getVulnerability(); + final Map analysis = finding.getAnalysis(); + final Map attribution = finding.getAttribution(); + + return new FindingResponse( + new Component( + value(component, "uuid", UUID.class), + value(component, "name", String.class), + value(component, "group", String.class), + value(component, "version", String.class), + value(component, "purl", String.class), + value(component, "cpe", String.class), + value(component, "project", UUID.class), + Boolean.TRUE.equals(component.get("hasOccurrences")), + enumValue(component, "scope", Scope.class), + value(component, "projectName", String.class), + value(component, "projectVersion", String.class)), + new VulnerabilityDetails( + value(vulnerability, "uuid", UUID.class), + enumValue(vulnerability, "source", Source.class), + value(vulnerability, "vulnId", String.class), + value(vulnerability, "title", String.class), + value(vulnerability, "subtitle", String.class), + value(vulnerability, "description", String.class), + value(vulnerability, "recommendation", String.class), + value(vulnerability, "references", String.class), + enumValue(vulnerability, "severity", Severity.class), + value(vulnerability, "severityRank", Integer.class), + value(vulnerability, "cvssV2BaseScore", BigDecimal.class), + value(vulnerability, "cvssV3BaseScore", BigDecimal.class), + value(vulnerability, "cvssV4Score", BigDecimal.class), + value(vulnerability, "cvssV2Vector", String.class), + value(vulnerability, "cvssV3Vector", String.class), + value(vulnerability, "cvssV4Vector", String.class), + value(vulnerability, "owaspLikelihoodScore", BigDecimal.class), + value(vulnerability, "owaspTechnicalImpactScore", BigDecimal.class), + value(vulnerability, "owaspBusinessImpactScore", BigDecimal.class), + value(vulnerability, "owaspRRVector", String.class), + value(vulnerability, "epssScore", BigDecimal.class), + value(vulnerability, "epssPercentile", BigDecimal.class), + cwes(vulnerability.get("cwes")), + aliases(vulnerability.get("aliases")), + timestamp(vulnerability.get("published"))), + new Analysis( + enumValue(analysis, "state", AnalysisState.class), + Boolean.TRUE.equals(analysis.get("isSuppressed"))), + new Attribution( + value(attribution, "analyzerIdentity", String.class), + timestamp(attribution.get("attributedOn")), + value(attribution, "alternateIdentifier", String.class), + value(attribution, "referenceUrl", String.class)), + finding.getMatrix()); + } + + private static @Nullable T value(final Map values, final String key, final Class type) { + return type.cast(values.get(key)); + } + + private static > @Nullable E enumValue( + final Map values, + final String key, + final Class type) { + final Object value = values.get(key); + if (value == null) { + return null; + } + return value instanceof String stringValue ? Enum.valueOf(type, stringValue) : type.cast(value); + } + + private static @Nullable Long timestamp(final @Nullable Object value) { + return value instanceof Date date ? date.getTime() : null; + } + + private static @Nullable List cwes(final @Nullable Object value) { + if (!(value instanceof List cwes)) { + return null; + } + return cwes.stream() + .map(org.dependencytrack.model.Cwe.class::cast) + .map(cwe -> new Cwe(cwe.getCweId(), cwe.getName())) + .toList(); + } + + private static Set aliases(final @Nullable Object value) { + if (!(value instanceof Set aliases)) { + return Set.of(); + } + return aliases.stream() + .map(Map.class::cast) + .map(alias -> new Alias( + (String) alias.get("cveId"), + (String) alias.get("ghsaId"), + (String) alias.get("sonatypeId"), + (String) alias.get("osvId"), + (String) alias.get("snykId"), + (String) alias.get("vulnDbId"))) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); + } + @Schema(name = "FindingComponent") public record Component( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID uuid, diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java deleted file mode 100644 index cd242477e0..0000000000 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/FindingOpenApiSchemaTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.resources.v1; - -import io.swagger.parser.OpenAPIParser; -import io.swagger.v3.oas.models.media.Schema; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Set; - -import static java.util.Objects.requireNonNull; -import static org.assertj.core.api.Assertions.assertThat; - -class FindingOpenApiSchemaTest { - - @Test - void shouldDescribeFindingResponse() throws IOException { - final String spec; - try (final var inputStream = FindingOpenApiSchemaTest.class - .getResourceAsStream("/org/dependencytrack/api/v1/openapi.yaml")) { - spec = new String(requireNonNull(inputStream).readAllBytes(), StandardCharsets.UTF_8); - } - - final var parseResult = new OpenAPIParser().readContents(spec, null, null); - assertThat(parseResult.getMessages()).isEmpty(); - - final var openApi = parseResult.getOpenAPI(); - final Schema responseSchema = openApi.getPaths() - .get("/v1/finding/project/{uuid}") - .getGet() - .getResponses() - .get("200") - .getContent() - .get("application/json") - .getSchema(); - assertThat(responseSchema.getItems().get$ref()).isEqualTo("#/components/schemas/Finding"); - - final var schemas = openApi.getComponents().getSchemas(); - assertThat(propertyNames(schemas.get("Finding"))) - .contains("component", "vulnerability", "analysis", "attribution", "matrix"); - assertThat(propertyNames(schemas.get("FindingComponent"))) - .contains("uuid", "name", "version", "project", "hasOccurrences"); - assertThat(propertyNames(schemas.get("FindingVulnerability"))) - .contains("uuid", "source", "vulnId", "severity", "cwes", "aliases"); - assertThat(propertyNames(schemas.get("FindingAnalysis"))) - .contains("state", "isSuppressed"); - assertThat(propertyNames(schemas.get("FindingAttribution"))) - .contains("analyzerIdentity", "attributedOn", "alternateIdentifier", "referenceUrl"); - } - - private static Set propertyNames(final Schema schema) { - return schema.getProperties().keySet(); - } -} From 36126329512ae9e9b8be3144910f90f9c979c86a Mon Sep 17 00:00:00 2001 From: omobolaji adeyan Date: Tue, 23 Jun 2026 18:49:56 -0500 Subject: [PATCH 3/3] Fix finding response compatibility Signed-off-by: omobolaji adeyan --- .../resources/v1/vo/FindingResponse.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java index 7088a442b9..12ef52175e 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/FindingResponse.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.resources.v1.vo; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.dependencytrack.model.AnalysisState; @@ -39,6 +40,7 @@ /// /// @since 5.1.0 @NullMarked +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(name = "Finding", description = "A vulnerability finding for a project component") public record FindingResponse( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Component component, @@ -66,7 +68,8 @@ public static FindingResponse of(final Finding finding) { Boolean.TRUE.equals(component.get("hasOccurrences")), enumValue(component, "scope", Scope.class), value(component, "projectName", String.class), - value(component, "projectVersion", String.class)), + value(component, "projectVersion", String.class), + value(component, "latestVersion", String.class)), new VulnerabilityDetails( value(vulnerability, "uuid", UUID.class), enumValue(vulnerability, "source", Source.class), @@ -150,6 +153,7 @@ private static Set aliases(final @Nullable Object value) { } @Schema(name = "FindingComponent") + @JsonInclude(JsonInclude.Include.NON_NULL) public record Component( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID uuid, @Nullable String name, @@ -162,10 +166,12 @@ public record Component( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean hasOccurrences, @Nullable Scope scope, @Nullable String projectName, - @Nullable String projectVersion) { + @Nullable String projectVersion, + @Nullable String latestVersion) { } @Schema(name = "FindingVulnerability") + @JsonInclude(JsonInclude.Include.NON_NULL) public record VulnerabilityDetails( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID uuid, @Nullable Source source, @@ -196,12 +202,14 @@ public record VulnerabilityDetails( } @Schema(name = "FindingCwe") + @JsonInclude(JsonInclude.Include.NON_NULL) public record Cwe( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int cweId, @Nullable String name) { } @Schema(name = "FindingVulnerabilityAlias") + @JsonInclude(JsonInclude.Include.NON_NULL) public record Alias( @Nullable String cveId, @Nullable String ghsaId, @@ -212,6 +220,7 @@ public record Alias( } @Schema(name = "FindingAnalysis") + @JsonInclude(JsonInclude.Include.NON_NULL) public record Analysis( @Nullable AnalysisState state, @JsonProperty("isSuppressed") @@ -219,6 +228,7 @@ public record Analysis( } @Schema(name = "FindingAttribution") + @JsonInclude(JsonInclude.Include.NON_NULL) public record Attribution( @Nullable String analyzerIdentity, @Schema(type = "integer", format = "int64", description = "Attribution timestamp in milliseconds since the Unix epoch")