Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ default Vulnerability getByVulnIdAndSource(final String vulnId, final String sou

@SqlQuery(/* language=InjectedFreeMarker */ """
<#-- @ftlvariable name="activeFilter" type="Boolean" -->
<#-- @ftlvariable name="searchText" type="Boolean" -->
<#-- @ftlvariable name="apiOrderByClause" type="String" -->
<#-- @ftlvariable name="apiOffsetLimitClause" type="String" -->
<#-- @ftlvariable name="apiProjectAclCondition" type="String" -->
Expand Down Expand Up @@ -200,6 +201,11 @@ WHERE EXISTS(
SELECT 1
FROM "CTE_AFFECTED_COMPONENTS"
WHERE "PROJECT_ID" = "PROJECT"."ID")
<#if searchText>
AND (
LOWER("PROJECT"."NAME") LIKE ('%' || LOWER(:searchText) || '%')
)
</#if>
<#if apiOrderByClause??>
${apiOrderByClause}
<#else>
Expand All @@ -217,7 +223,8 @@ WHERE EXISTS(
List<AffectedProjectListRow> getAffectedProjects(
@Bind String source,
@Bind String vulnId,
@Bind Boolean activeFilter);
@Bind Boolean activeFilter,
@Bind @Nullable String searchText);

record AffectedProjectListRow(
UUID uuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
package org.dependencytrack.resources.v1;

import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import alpine.server.auth.PermissionRequired;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
Expand Down Expand Up @@ -256,7 +258,18 @@ public Response getVulnerabilityByVulnId(
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Returns a list of all projects affected by a specific vulnerability",
description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
description = """
<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>\
<p>Optional query parameter <code>searchText</code>. The search is case insensitive.
So if a search is done for project 'abc' and a project named 'aBc' exists in the knowledgebase,
then the latter would appear in the search results</p>""",
parameters = {
@Parameter(
name = "searchText",
in = ParameterIn.QUERY,
description = "Optional case-insensitive substring match on project name."
)
}
)
@PaginatedApi
@ApiResponses(value = {
Expand All @@ -273,8 +286,12 @@ public Response getAffectedProject(@PathParam("source") String source,
@PathParam("vuln") String vuln,
@Parameter(description = "Optionally excludes inactive projects from being returned", required = false)
@QueryParam("excludeInactive") boolean excludeInactive) {
final List<AffectedProjectListRow> affectedProjectRows = withJdbiHandle(getAlpineRequest(), handle ->
handle.attach(VulnerabilityDao.class).getAffectedProjects(source, vuln, excludeInactive ? true : null));
final AlpineRequest alpineRequest = getAlpineRequest();
final String affectedProjectsFilter;
final String filter = alpineRequest.getFilter();
affectedProjectsFilter = (filter == null || filter.isBlank()) ? null : filter;
final List<AffectedProjectListRow> affectedProjectRows = withJdbiHandle(alpineRequest, handle ->
handle.attach(VulnerabilityDao.class).getAffectedProjects(source, vuln, excludeInactive ? true : null, affectedProjectsFilter));

final long totalCount = affectedProjectRows.isEmpty() ? 0 : affectedProjectRows.getFirst().totalCount();
final List<AffectedProject> affectedProjects = affectedProjectRows.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,50 @@ public void getAffectedProjectTest() {
""");
}

@Test
public void getAffectedProjectWithSearchTextTest() {
initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
final var sampleData = new SampleData();
final Response response = jersey.target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId() + "/projects")
.queryParam("searchText", "Project 1")
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
Assertions.assertEquals(200, response.getStatus(), 0);
Assertions.assertEquals(String.valueOf(1), response.getHeaderString(TOTAL_COUNT_HEADER));
assertThatJson(getPlainTextBody(response))
.withMatcher("projectUuid", equalTo(sampleData.p1.getUuid().toString()))
.withMatcher("componentUuid", equalTo(sampleData.c1.getUuid().toString()))
.isEqualTo(/* language=JSON */ """
[
{
"active": true,
"affectedComponentUuids": [
"${json-unit.matches:componentUuid}"
],
"dependencyGraphAvailable": false,
"name": "Project 1",
"uuid": "${json-unit.matches:projectUuid}",
"version": null
}
]
""");
}

@Test
public void getAffectedProjectWithSearchTextNoMatchTest() {
initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
final var sampleData = new SampleData();
final Response response = jersey.target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId() + "/projects")
.queryParam("searchText", "does-not-exist")
.request()
.header(X_API_KEY, apiKey)
.get(Response.class);
Assertions.assertEquals(200, response.getStatus(), 0);
Assertions.assertEquals("0", response.getHeaderString(TOTAL_COUNT_HEADER));
Assertions.assertEquals("[]", getPlainTextBody(response));
}

@Test
public void getAffectedProjectWithDeletedFindingTest() {
initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
Expand Down
Loading