Skip to content

Commit

Permalink
Fix /api/v1/vulnerability/source/{source}/vuln/{vuln} returning all…
Browse files Browse the repository at this point in the history
… vulnerable components

Fixes DependencyTrack/hyades#1539

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Sep 27, 2024
1 parent 816b742 commit c4b68d8
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import org.dependencytrack.model.Component;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.persistence.jdbi.mapping.VulnerabilityRowMapper;
import org.dependencytrack.persistence.jdbi.mapping.VulnerableSoftwareRowMapper;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.config.RegisterFieldMapper;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
Expand All @@ -37,6 +39,108 @@
*/
public interface VulnerabilityDao {

@SqlQuery(/* language=InjectedFreeMarker */ """
<#-- @ftlvariable name="uuid" type="Boolean" -->
SELECT "VULNERABILITY"."ID" AS "ID"
, "VULNID"
, "SOURCE"
, "FRIENDLYVULNID"
, "TITLE"
, "SUBTITLE"
, "DESCRIPTION"
, "DETAIL"
, "RECOMMENDATION"
, "REFERENCES"
, "CREDITS"
, "CREATED"
, "PUBLISHED"
, "UPDATED"
, STRING_TO_ARRAY("CWES", ',') AS "CWES"
, "CVSSV2BASESCORE"
, "CVSSV2IMPACTSCORE"
, "CVSSV2EXPLOITSCORE"
, "CVSSV2VECTOR"
, "CVSSV3BASESCORE"
, "CVSSV3IMPACTSCORE"
, "CVSSV3EXPLOITSCORE"
, "CVSSV3VECTOR"
, "OWASPRRLIKELIHOODSCORE"
, "OWASPRRTECHNICALIMPACTSCORE"
, "OWASPRRBUSINESSIMPACTSCORE"
, "OWASPRRVECTOR"
, "SEVERITY"
, "VULNERABLEVERSIONS"
, "PATCHEDVERSIONS"
, "UUID"
, JSONB_VULN_ALIASES("SOURCE", "VULNID") AS "vulnAliasesJson"
FROM "VULNERABILITY"
LEFT JOIN "EPSS"
ON "VULNID" = "EPSS"."CVE"
WHERE 1=1
<#if uuid>
AND "UUID" = :uuid
<#else>
AND "VULNID" = :vulnId
AND "SOURCE" = :source
</#if>
""")
@DefineNamedBindings
@RegisterRowMapper(VulnerabilityRowMapper.class)
Vulnerability getByUuidOrVulnIdAndSource(@Bind UUID uuid, @Bind String vulnId, @Bind String source);

default Vulnerability getByUuid(final UUID uuid) {
return getByUuidOrVulnIdAndSource(uuid, null, null);
}

default Vulnerability getByVulnIdAndSource(final String vulnId, final String source) {
return getByUuidOrVulnIdAndSource(null, vulnId, source);
}

@SqlQuery("""
SELECT "VS"."ID"
, "VS"."PURL"
, "VS"."PURL_TYPE"
, "VS"."PURL_NAMESPACE"
, "VS"."PURL_NAME"
, "VS"."PURL_VERSION"
, "VS"."PURL_QUALIFIERS"
, "VS"."PURL_SUBPATH"
, "VS"."CPE22"
, "VS"."CPE23"
, "VS"."PART"
, "VS"."VENDOR"
, "VS"."PRODUCT"
, "VS"."VERSION"
, "VS"."UPDATE"
, "VS"."EDITION"
, "VS"."LANGUAGE"
, "VS"."SWEDITION"
, "VS"."TARGETSW"
, "VS"."TARGETHW"
, "VS"."OTHER"
, "VS"."VERSIONENDEXCLUDING"
, "VS"."VERSIONENDINCLUDING"
, "VS"."VERSIONSTARTEXCLUDING"
, "VS"."VERSIONSTARTINCLUDING"
, "VS"."VULNERABLE"
, "VS"."UUID"
, (SELECT JSONB_AGG(JSONB_BUILD_OBJECT(
'id', "ID"
, 'firstSeen', EXTRACT(EPOCH FROM "FIRST_SEEN") * 1000
, 'lastSeen', EXTRACT(EPOCH FROM "LAST_SEEN") * 1000
, 'source', "SOURCE"
, 'uuid', "UUID"))
FROM "AFFECTEDVERSIONATTRIBUTION"
WHERE "VULNERABILITY" = "VSV"."VULNERABILITY_ID"
AND "VULNERABLE_SOFTWARE" = "VSV"."VULNERABLESOFTWARE_ID") AS "attributionsJson"
FROM "VULNERABLESOFTWARE_VULNERABILITIES" AS "VSV"
INNER JOIN "VULNERABLESOFTWARE" AS "VS"
ON "VS"."ID" = "VSV"."VULNERABLESOFTWARE_ID"
WHERE "VSV"."VULNERABILITY_ID" = :id
""")
@RegisterRowMapper(VulnerableSoftwareRowMapper.class)
List<VulnerableSoftware> getVulnerableSoftwareByVulnId(@Bind long id);

@SqlQuery(/* language=InjectedFreeMarker */ """
<#-- @ftlvariable name="activeFilter" type="Boolean" -->
<#-- @ftlvariable name="apiOrderByClause" type="String" -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.persistence.jdbi.mapping;

import com.fasterxml.jackson.core.type.TypeReference;
import org.dependencytrack.model.AffectedVersionAttribution;
import org.dependencytrack.model.VulnerableSoftware;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.mapper.reflect.BeanMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.deserializeJson;
import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet;

/**
* @since 5.6.0
*/
public class VulnerableSoftwareRowMapper implements RowMapper<VulnerableSoftware> {

private static final TypeReference<List<AffectedVersionAttribution>> ATTRIBUTIONS_TYPE_REF = new TypeReference<>() {
};

private final RowMapper<VulnerableSoftware> vsBeanMapper = BeanMapper.of(VulnerableSoftware.class);

@Override
public VulnerableSoftware map(final ResultSet rs, final StatementContext ctx) throws SQLException {
final VulnerableSoftware vs = this.vsBeanMapper.map(rs, ctx);
maybeSet(rs, "attributionsJson",
(ignored, columnName) -> deserializeJson(rs, columnName, ATTRIBUTIONS_TYPE_REF),
vs::setAffectedVersionAttributions);
return vs;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import jakarta.validation.Validator;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
Expand All @@ -44,7 +45,6 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.AffectedVersionAttribution;
import org.dependencytrack.model.AnalyzerIdentity;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Cwe;
Expand All @@ -69,6 +69,7 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;

Expand Down Expand Up @@ -181,16 +182,28 @@ public Response getVulnerabilitiesByProject(@Parameter(description = "The UUID o
@ApiResponse(responseCode = "404", description = "The vulnerability could not be found")
})
@PermissionRequired({Permissions.Constants.VULNERABILITY_MANAGEMENT, Permissions.Constants.VULNERABILITY_MANAGEMENT_READ})
public Response getVulnerabilityByUuid(@Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true)
@PathParam("uuid") @ValidUuid String uuid) {
try (QueryManager qm = new QueryManager()) {
final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid);
if (vulnerability != null) {
return Response.ok(vulnerability).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
public Response getVulnerabilityByUuid(
@Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true)
@PathParam("uuid") @ValidUuid final String uuid) {
return withJdbiHandle(getAlpineRequest(), handle -> {
final var dao = handle.attach(VulnerabilityDao.class);

final Vulnerability vuln = dao.getByUuid(UUID.fromString(uuid));
if (vuln == null) {
throw new ClientErrorException(Response
.status(Response.Status.NOT_FOUND)
.entity("The vulnerability could not be found.")
.build());
}
}

final List<VulnerableSoftware> vsList = dao.getVulnerableSoftwareByVulnId(vuln.getId());
final List<AffectedComponent> affectedComponents = vsList.stream().map(AffectedComponent::new).toList();
if (!affectedComponents.isEmpty()) {
vuln.setAffectedComponents(affectedComponents);
}

return Response.status(Response.Status.OK).entity(vuln).build();
});
}

@GET
Expand All @@ -210,24 +223,28 @@ public Response getVulnerabilityByUuid(@Parameter(description = "The UUID of the
@ApiResponse(responseCode = "404", description = "The vulnerability could not be found")
})
@PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
public Response getVulnerabilityByVulnId(@PathParam("source") String source,
@PathParam("vuln") String vuln) {
try (QueryManager qm = new QueryManager()) {
final Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vuln);
if (vulnerability != null) {
final List<AffectedComponent> affectedComponents = new ArrayList<>();
for (final VulnerableSoftware vs : vulnerability.getVulnerableSoftware()) {
AffectedComponent affectedComponent = new AffectedComponent(vs);
final List<AffectedVersionAttribution> attributions = qm.getAffectedVersionAttributions(vulnerability, vs);
affectedComponent.setAffectedVersionAttributions(attributions);
affectedComponents.add(affectedComponent);
}
vulnerability.setAffectedComponents(affectedComponents);
return Response.ok(vulnerability).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
public Response getVulnerabilityByVulnId(
@PathParam("source") final String source,
@PathParam("vuln") final String vulnId) {
return withJdbiHandle(getAlpineRequest(), handle -> {
final var dao = handle.attach(VulnerabilityDao.class);

final Vulnerability vuln = dao.getByVulnIdAndSource(vulnId, source);
if (vuln == null) {
throw new ClientErrorException(Response
.status(Response.Status.NOT_FOUND)
.entity("The vulnerability could not be found.")
.build());
}
}

final List<VulnerableSoftware> vsList = dao.getVulnerableSoftwareByVulnId(vuln.getId());
final List<AffectedComponent> affectedComponents = vsList.stream().map(AffectedComponent::new).toList();
if (!affectedComponents.isEmpty()) {
vuln.setAffectedComponents(affectedComponents);
}

return Response.status(Response.Status.OK).entity(vuln).build();
});
}

@GET
Expand Down
Loading

0 comments on commit c4b68d8

Please sign in to comment.