Skip to content
Open
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,10 +19,12 @@
package org.dependencytrack.resources.v1;

import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import alpine.server.auth.PermissionRequired;
import alpine.server.filters.ResourceAccessRequired;
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 @@ -260,7 +262,22 @@ 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 parameters <code>searchText</code> or <code>filter</code> narrow the list; \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's commit to only searchText to avoid confusion. Also, when we explicitly mention it, we should also note what it searches (i.e. project name, case-insensitive, "contains" semantics).

both are provided by the Alpine request filter and match the same value.</p>""",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an implementation detail that doesn't belong in public API documentation.

parameters = {
@Parameter(
name = "searchText",
in = ParameterIn.QUERY,
description = "Optional case-insensitive substring match on project name."
),
@Parameter(
name = "filter",
in = ParameterIn.QUERY,
description = "Optional filter (Alpine); same effect as <code>searchText</code> for this endpoint."
)
}
)
@PaginatedApi
@ApiResponses(value = {
Expand All @@ -278,8 +295,16 @@ 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;
if (alpineRequest == null) {
Comment on lines +298 to +300
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAlpineRequest should never return null, no need to handle that here.

affectedProjectsFilter = null;
} else {
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 @@ -627,6 +627,48 @@ public void getAffectedProjectTest() {
""");
}

@Test
public void getAffectedProjectWithSearchTextTest() {
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() {
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() {
var project = qm.createProject("Project 1", null, null, null, null, null, null, false);
Expand Down
Loading